Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.flayignore1
-rw-r--r--.gitignore3
-rw-r--r--.gitlab-ci.yml58
-rw-r--r--.rubocop.yml83
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG227
-rw-r--r--CONTRIBUTING.md395
-rw-r--r--GITLAB_GIT_HTTP_SERVER_VERSION1
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION1
-rw-r--r--Gemfile108
-rw-r--r--Gemfile.lock529
-rw-r--r--LICENSE2
-rw-r--r--PROCESS.md33
-rw-r--r--Procfile6
-rw-r--r--README.md6
-rw-r--r--VERSION2
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Black.ttfbin148368 -> 289364 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttfbin0 -> 103404 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Bold.ttfbin291424 -> 291424 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttfbin0 -> 103608 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-ExtraLight.ttfbin150528 -> 291652 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttfbin0 -> 104768 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttfbin0 -> 104236 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Light.ttfbin293220 -> 293220 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttfbin0 -> 104616 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Regular.ttfbin293956 -> 293956 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/fonts/SourceSansPro-Semibold.ttfbin292404 -> 292404 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttfbin0 -> 104020 bytes
-rw-r--r--app/assets/images/auth_buttons/facebook_64.pngbin0 -> 2970 bytes
-rw-r--r--app/assets/images/brand_logo.pngbin27059 -> 0 bytes
-rw-r--r--app/assets/images/emoji.pngbin0 -> 832902 bytes
-rw-r--r--app/assets/images/gitlab_logo.pngbin0 -> 5189 bytes
-rw-r--r--app/assets/images/icon-link.pngbin726 -> 1128 bytes
-rw-r--r--app/assets/javascripts/api.js.coffee31
-rw-r--r--app/assets/javascripts/application.js.coffee34
-rw-r--r--app/assets/javascripts/awards_handler.coffee166
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js.coffee17
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee8
-rw-r--r--app/assets/javascripts/blob/new_blob.js.coffee8
-rw-r--r--app/assets/javascripts/calendar.js.coffee7
-rw-r--r--app/assets/javascripts/ci/build.coffee4
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js.coffee37
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee12
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee80
-rw-r--r--app/assets/javascripts/flash.js.coffee16
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee10
-rw-r--r--app/assets/javascripts/issue.js.coffee42
-rw-r--r--app/assets/javascripts/issues.js.coffee13
-rw-r--r--app/assets/javascripts/logo.js.coffee43
-rw-r--r--app/assets/javascripts/markdown_preview.js.coffee87
-rw-r--r--app/assets/javascripts/merge_request.js.coffee8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee29
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee9
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee4
-rw-r--r--app/assets/javascripts/new_branch_form.js.coffee78
-rw-r--r--app/assets/javascripts/new_commit_form.js.coffee21
-rw-r--r--app/assets/javascripts/notes.js.coffee56
-rw-r--r--app/assets/javascripts/project.js.coffee28
-rw-r--r--app/assets/javascripts/project_select.js.coffee39
-rw-r--r--app/assets/javascripts/projects_list.js.coffee10
-rw-r--r--app/assets/javascripts/shortcuts.js.coffee10
-rw-r--r--app/assets/javascripts/sidebar.js.coffee1
-rw-r--r--app/assets/javascripts/star.js.coffee22
-rw-r--r--app/assets/javascripts/stat_graph_contributors_util.js.coffee5
-rw-r--r--app/assets/javascripts/user.js.coffee6
-rw-r--r--app/assets/javascripts/users_select.js.coffee30
-rw-r--r--app/assets/stylesheets/application.scss6
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/blocks.scss21
-rw-r--r--app/assets/stylesheets/framework/buttons.scss54
-rw-r--r--app/assets/stylesheets/framework/calendar.scss42
-rw-r--r--app/assets/stylesheets/framework/callout.scss11
-rw-r--r--app/assets/stylesheets/framework/common.scss97
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss21
-rw-r--r--app/assets/stylesheets/framework/header.scss12
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss19
-rw-r--r--app/assets/stylesheets/framework/layout.scss12
-rw-r--r--app/assets/stylesheets/framework/lists.scss54
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss10
-rw-r--r--app/assets/stylesheets/framework/mixins.scss19
-rw-r--r--app/assets/stylesheets/framework/mobile.scss5
-rw-r--r--app/assets/stylesheets/framework/pagination.scss4
-rw-r--r--app/assets/stylesheets/framework/panels.scss20
-rw-r--r--app/assets/stylesheets/framework/selects.scss62
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss192
-rw-r--r--app/assets/stylesheets/framework/tables.scss2
-rw-r--r--app/assets/stylesheets/framework/timeline.scss3
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss18
-rw-r--r--app/assets/stylesheets/framework/variables.scss33
-rw-r--r--app/assets/stylesheets/pages/awards.scss125
-rw-r--r--app/assets/stylesheets/pages/builds.scss9
-rw-r--r--app/assets/stylesheets/pages/commit.scss20
-rw-r--r--app/assets/stylesheets/pages/commits.scss65
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss33
-rw-r--r--app/assets/stylesheets/pages/diff.scss1
-rw-r--r--app/assets/stylesheets/pages/editor.scss46
-rw-r--r--app/assets/stylesheets/pages/emojis.scss1272
-rw-r--r--app/assets/stylesheets/pages/events.scss14
-rw-r--r--app/assets/stylesheets/pages/groups.scss7
-rw-r--r--app/assets/stylesheets/pages/issuable.scss101
-rw-r--r--app/assets/stylesheets/pages/issues.scss36
-rw-r--r--app/assets/stylesheets/pages/login.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss73
-rw-r--r--app/assets/stylesheets/pages/note_form.scss19
-rw-r--r--app/assets/stylesheets/pages/notes.scss12
-rw-r--r--app/assets/stylesheets/pages/profile.scss28
-rw-r--r--app/assets/stylesheets/pages/projects.scss185
-rw-r--r--app/assets/stylesheets/pages/runners.scss52
-rw-r--r--app/assets/stylesheets/pages/sherlock.scss33
-rw-r--r--app/assets/stylesheets/pages/snippets.scss32
-rw-r--r--app/assets/stylesheets/pages/status.scss17
-rw-r--r--app/assets/stylesheets/pages/tree.scss2
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss5
-rw-r--r--app/assets/stylesheets/pages/wiki.scss5
-rw-r--r--app/controllers/abuse_reports_controller.rb11
-rw-r--r--app/controllers/admin/application_controller.rb6
-rw-r--r--app/controllers/admin/application_settings_controller.rb21
-rw-r--r--app/controllers/admin/builds_controller.rb23
-rw-r--r--app/controllers/admin/identities_controller.rb17
-rw-r--r--app/controllers/admin/impersonation_controller.rb38
-rw-r--r--app/controllers/admin/runner_projects_controller.rb35
-rw-r--r--app/controllers/admin/runners_controller.rb63
-rw-r--r--app/controllers/admin/users_controller.rb6
-rw-r--r--app/controllers/application_controller.rb62
-rw-r--r--app/controllers/autocomplete_controller.rb51
-rw-r--r--app/controllers/ci/admin/application_controller.rb10
-rw-r--r--app/controllers/ci/admin/application_settings_controller.rb31
-rw-r--r--app/controllers/ci/admin/builds_controller.rb18
-rw-r--r--app/controllers/ci/admin/events_controller.rb9
-rw-r--r--app/controllers/ci/admin/projects_controller.rb19
-rw-r--r--app/controllers/ci/admin/runner_projects_controller.rb34
-rw-r--r--app/controllers/ci/admin/runners_controller.rb72
-rw-r--r--app/controllers/ci/application_controller.rb26
-rw-r--r--app/controllers/ci/events_controller.rb21
-rw-r--r--app/controllers/ci/lints_controller.rb10
-rw-r--r--app/controllers/ci/projects_controller.rb15
-rw-r--r--app/controllers/ci/runner_projects_controller.rb36
-rw-r--r--app/controllers/concerns/creates_commit.rb103
-rw-r--r--app/controllers/concerns/global_milestones.rb21
-rw-r--r--app/controllers/concerns/issues_action.rb14
-rw-r--r--app/controllers/concerns/merge_requests_action.rb9
-rw-r--r--app/controllers/dashboard/milestones_controller.rb29
-rw-r--r--app/controllers/dashboard/snippets_controller.rb3
-rw-r--r--app/controllers/dashboard_controller.rb25
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/groups/application_controller.rb11
-rw-r--r--app/controllers/groups/avatars_controller.rb4
-rw-r--r--app/controllers/groups/group_members_controller.rb35
-rw-r--r--app/controllers/groups/milestones_controller.rb63
-rw-r--r--app/controllers/groups_controller.rb26
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb16
-rw-r--r--app/controllers/passwords_controller.rb6
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb25
-rw-r--r--app/controllers/profiles_controller.rb1
-rw-r--r--app/controllers/projects/application_controller.rb10
-rw-r--r--app/controllers/projects/blob_controller.rb83
-rw-r--r--app/controllers/projects/branches_controller.rb7
-rw-r--r--app/controllers/projects/builds_controller.rb65
-rw-r--r--app/controllers/projects/ci_services_controller.rb49
-rw-r--r--app/controllers/projects/ci_settings_controller.rb36
-rw-r--r--app/controllers/projects/ci_web_hooks_controller.rb45
-rw-r--r--app/controllers/projects/commit_controller.rb53
-rw-r--r--app/controllers/projects/commits_controller.rb4
-rw-r--r--app/controllers/projects/compare_controller.rb7
-rw-r--r--app/controllers/projects/forks_controller.rb28
-rw-r--r--app/controllers/projects/graphs_controller.rb32
-rw-r--r--app/controllers/projects/hooks_controller.rb8
-rw-r--r--app/controllers/projects/imports_controller.rb29
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb85
-rw-r--r--app/controllers/projects/notes_controller.rb54
-rw-r--r--app/controllers/projects/project_members_controller.rb36
-rw-r--r--app/controllers/projects/protected_branches_controller.rb2
-rw-r--r--app/controllers/projects/raw_controller.rb41
-rw-r--r--app/controllers/projects/releases_controller.rb31
-rw-r--r--app/controllers/projects/runner_projects_controller.rb26
-rw-r--r--app/controllers/projects/runners_controller.rb20
-rw-r--r--app/controllers/projects/services_controller.rb9
-rw-r--r--app/controllers/projects/snippets_controller.rb1
-rw-r--r--app/controllers/projects/tags_controller.rb22
-rw-r--r--app/controllers/projects/tree_controller.rb31
-rw-r--r--app/controllers/projects/triggers_controller.rb9
-rw-r--r--app/controllers/projects/variables_controller.rb5
-rw-r--r--app/controllers/projects_controller.rb22
-rw-r--r--app/controllers/registrations_controller.rb23
-rw-r--r--app/controllers/search_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb18
-rw-r--r--app/controllers/sherlock/application_controller.rb12
-rw-r--r--app/controllers/sherlock/file_samples_controller.rb7
-rw-r--r--app/controllers/sherlock/queries_controller.rb7
-rw-r--r--app/controllers/sherlock/transactions_controller.rb19
-rw-r--r--app/controllers/snippets_controller.rb9
-rw-r--r--app/controllers/users_controller.rb29
-rw-r--r--app/finders/contributed_projects_finder.rb37
-rw-r--r--app/finders/groups_finder.rb38
-rw-r--r--app/finders/issuable_finder.rb20
-rw-r--r--app/finders/milestones_finder.rb12
-rw-r--r--app/finders/notes_finder.rb4
-rw-r--r--app/finders/personal_projects_finder.rb41
-rw-r--r--app/finders/projects_finder.rb121
-rw-r--r--app/helpers/application_helper.rb20
-rw-r--r--app/helpers/auth_helper.rb14
-rw-r--r--app/helpers/blob_helper.rb124
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/builds_helper.rb13
-rw-r--r--app/helpers/button_helper.rb58
-rw-r--r--app/helpers/ci/gitlab_helper.rb36
-rw-r--r--app/helpers/ci/projects_helper.rb36
-rw-r--r--app/helpers/ci_badge_helper.rb13
-rw-r--r--app/helpers/ci_status_helper.rb42
-rw-r--r--app/helpers/commits_helper.rb4
-rw-r--r--app/helpers/diff_helper.rb42
-rw-r--r--app/helpers/emails_helper.rb2
-rw-r--r--app/helpers/events_helper.rb28
-rw-r--r--app/helpers/external_wiki_helper.rb2
-rw-r--r--app/helpers/gitlab_markdown_helper.rb53
-rw-r--r--app/helpers/gitlab_routing_helper.rb2
-rw-r--r--app/helpers/graph_helper.rb10
-rw-r--r--app/helpers/icons_helper.rb22
-rw-r--r--app/helpers/issues_helper.rb75
-rw-r--r--app/helpers/labels_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb36
-rw-r--r--app/helpers/milestones_helper.rb4
-rw-r--r--app/helpers/namespaces_helper.rb15
-rw-r--r--app/helpers/nav_helper.rb8
-rw-r--r--app/helpers/notifications_helper.rb38
-rw-r--r--app/helpers/page_layout_helper.rb77
-rw-r--r--app/helpers/projects_helper.rb53
-rw-r--r--app/helpers/runners_helper.rb2
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/selects_helper.rb43
-rw-r--r--app/helpers/tab_helper.rb18
-rw-r--r--app/helpers/tree_helper.rb47
-rw-r--r--app/helpers/triggers_helper.rb4
-rw-r--r--app/helpers/visibility_level_helper.rb67
-rw-r--r--app/mailers/abuse_report_mailer.rb10
-rw-r--r--app/mailers/base_mailer.rb4
-rw-r--r--app/mailers/ci/emails/builds.rb17
-rw-r--r--app/mailers/ci/notify.rb46
-rw-r--r--app/mailers/emails/builds.rb15
-rw-r--r--app/mailers/emails/issues.rb68
-rw-r--r--app/mailers/emails/notes.rb69
-rw-r--r--app/mailers/emails/projects.rb88
-rw-r--r--app/mailers/notify.rb7
-rw-r--r--app/models/ability.rb152
-rw-r--r--app/models/abuse_report.rb6
-rw-r--r--app/models/application_setting.rb118
-rw-r--r--app/models/broadcast_message.rb8
-rw-r--r--app/models/ci/application_setting.rb27
-rw-r--r--app/models/ci/build.rb171
-rw-r--r--app/models/ci/commit.rb69
-rw-r--r--app/models/ci/event.rb27
-rw-r--r--app/models/ci/project.rb215
-rw-r--r--app/models/ci/project_status.rb35
-rw-r--r--app/models/ci/runner.rb9
-rw-r--r--app/models/ci/runner_project.rb6
-rw-r--r--app/models/ci/service.rb105
-rw-r--r--app/models/ci/trigger.rb4
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--app/models/ci/web_hook.rb44
-rw-r--r--app/models/commit.rb50
-rw-r--r--app/models/commit_range.rb110
-rw-r--r--app/models/commit_status.rb46
-rw-r--r--app/models/concerns/issuable.rb64
-rw-r--r--app/models/concerns/mentionable.rb53
-rw-r--r--app/models/concerns/participable.rb31
-rw-r--r--app/models/concerns/referable.rb23
-rw-r--r--app/models/concerns/sortable.rb11
-rw-r--r--app/models/concerns/strip_attribute.rb34
-rw-r--r--app/models/concerns/taskable.rb33
-rw-r--r--app/models/concerns/token_authenticatable.rb50
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/generic_commit_status.rb33
-rw-r--r--app/models/global_label.rb17
-rw-r--r--app/models/global_milestone.rb (renamed from app/models/group_milestone.rb)53
-rw-r--r--app/models/group.rb16
-rw-r--r--app/models/group_label.rb9
-rw-r--r--app/models/hooks/project_hook.rb26
-rw-r--r--app/models/hooks/service_hook.rb25
-rw-r--r--app/models/hooks/system_hook.rb25
-rw-r--r--app/models/hooks/web_hook.rb65
-rw-r--r--app/models/identity.rb1
-rw-r--r--app/models/issue.rb12
-rw-r--r--app/models/jira_issue.rb2
-rw-r--r--app/models/label.rb7
-rw-r--r--app/models/lfs_object.rb32
-rw-r--r--app/models/lfs_objects_project.rb19
-rw-r--r--app/models/member.rb38
-rw-r--r--app/models/merge_request.rb95
-rw-r--r--app/models/merge_request_diff.rb16
-rw-r--r--app/models/milestone.rb9
-rw-r--r--app/models/namespace.rb19
-rw-r--r--app/models/note.rb101
-rw-r--r--app/models/project.rb177
-rw-r--r--app/models/project_services/asana_service.rb82
-rw-r--r--app/models/project_services/bamboo_service.rb13
-rw-r--r--app/models/project_services/buildkite_service.rb2
-rw-r--r--app/models/project_services/builds_email_service.rb90
-rw-r--r--app/models/project_services/ci/hip_chat_message.rb73
-rw-r--r--app/models/project_services/ci/hip_chat_service.rb93
-rw-r--r--app/models/project_services/ci/mail_service.rb84
-rw-r--r--app/models/project_services/ci/slack_message.rb92
-rw-r--r--app/models/project_services/ci/slack_service.rb81
-rw-r--r--app/models/project_services/drone_ci_service.rb34
-rw-r--r--app/models/project_services/external_wiki_service.rb6
-rw-r--r--app/models/project_services/flowdock_service.rb2
-rw-r--r--app/models/project_services/gemnasium_service.rb2
-rw-r--r--app/models/project_services/gitlab_ci_service.rb75
-rw-r--r--app/models/project_services/hipchat_service.rb49
-rw-r--r--app/models/project_services/jira_service.rb241
-rw-r--r--app/models/project_services/slack_service.rb29
-rw-r--r--app/models/project_services/slack_service/base_message.rb3
-rw-r--r--app/models/project_services/slack_service/build_message.rb82
-rw-r--r--app/models/project_services/slack_service/note_message.rb31
-rw-r--r--app/models/project_services/teamcity_service.rb10
-rw-r--r--app/models/project_wiki.rb10
-rw-r--r--app/models/release.rb17
-rw-r--r--app/models/repository.rb274
-rw-r--r--app/models/sent_notification.rb7
-rw-r--r--app/models/service.rb21
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/tree.rb14
-rw-r--r--app/models/user.rb138
-rw-r--r--app/models/users_star_project.rb2
-rw-r--r--app/services/base_service.rb5
-rw-r--r--app/services/ci/create_builds_service.rb6
-rw-r--r--app/services/ci/create_commit_service.rb28
-rw-r--r--app/services/ci/create_trigger_request_service.rb6
-rw-r--r--app/services/ci/event_service.rb31
-rw-r--r--app/services/ci/image_for_build_service.rb16
-rw-r--r--app/services/ci/register_build_service.rb9
-rw-r--r--app/services/ci/test_hook_service.rb7
-rw-r--r--app/services/compare_service.rb4
-rw-r--r--app/services/create_branch_service.rb22
-rw-r--r--app/services/create_commit_builds_service.rb42
-rw-r--r--app/services/create_release_service.rb31
-rw-r--r--app/services/create_tag_service.rb8
-rw-r--r--app/services/delete_branch_service.rb4
-rw-r--r--app/services/delete_tag_service.rb4
-rw-r--r--app/services/files/base_service.rb28
-rw-r--r--app/services/files/create_dir_service.rb11
-rw-r--r--app/services/files/create_service.rb13
-rw-r--r--app/services/git_hooks_service.rb28
-rw-r--r--app/services/git_push_service.rb13
-rw-r--r--app/services/git_tag_push_service.rb1
-rw-r--r--app/services/gravatar_service.rb4
-rw-r--r--app/services/issuable_base_service.rb49
-rw-r--r--app/services/issues/close_service.rb5
-rw-r--r--app/services/issues/update_service.rb51
-rw-r--r--app/services/labels/group_service.rb26
-rw-r--r--app/services/merge_requests/merge_service.rb18
-rw-r--r--app/services/merge_requests/merge_when_build_succeeds_service.rb55
-rw-r--r--app/services/merge_requests/refresh_service.rb89
-rw-r--r--app/services/merge_requests/update_service.rb72
-rw-r--r--app/services/milestones/group_service.rb26
-rw-r--r--app/services/notes/create_service.rb4
-rw-r--r--app/services/notes/update_service.rb2
-rw-r--r--app/services/notification_service.rb134
-rw-r--r--app/services/projects/create_service.rb16
-rw-r--r--app/services/projects/fork_service.rb15
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/update_service.rb28
-rw-r--r--app/services/system_hooks_service.rb81
-rw-r--r--app/services/system_note_service.rb45
-rw-r--r--app/services/update_release_service.rb29
-rw-r--r--app/uploaders/artifact_uploader.rb46
-rw-r--r--app/uploaders/attachment_uploader.rb19
-rw-r--r--app/uploaders/avatar_uploader.rb19
-rw-r--r--app/uploaders/file_uploader.rb19
-rw-r--r--app/uploaders/lfs_object_uploader.rb29
-rw-r--r--app/uploaders/uploader_helper.rb19
-rw-r--r--app/validators/color_validator.rb20
-rw-r--r--app/validators/email_validator.rb (renamed from lib/email_validator.rb)11
-rw-r--r--app/validators/line_code_validator.rb12
-rw-r--r--app/validators/namespace_name_validator.rb10
-rw-r--r--app/validators/namespace_validator.rb50
-rw-r--r--app/validators/url_validator.rb36
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml2
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml18
-rw-r--r--app/views/admin/abuse_reports/index.html.haml3
-rw-r--r--app/views/admin/application_settings/_form.html.haml107
-rw-r--r--app/views/admin/builds/_build.html.haml73
-rw-r--r--app/views/admin/builds/index.html.haml50
-rw-r--r--app/views/admin/dashboard/index.html.haml26
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/identities/index.html.haml1
-rw-r--r--app/views/admin/identities/new.html.haml4
-rw-r--r--app/views/admin/labels/_form.html.haml6
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/labels/edit.html.haml8
-rw-r--r--app/views/admin/labels/new.html.haml6
-rw-r--r--app/views/admin/runners/_runner.html.haml (renamed from app/views/ci/admin/runners/_runner.html.haml)12
-rw-r--r--app/views/admin/runners/index.html.haml (renamed from app/views/ci/admin/runners/index.html.haml)24
-rw-r--r--app/views/admin/runners/show.html.haml (renamed from app/views/ci/admin/runners/show.html.haml)55
-rw-r--r--app/views/admin/runners/update.js.haml (renamed from app/views/ci/admin/runners/update.js.haml)0
-rw-r--r--app/views/admin/users/_head.html.haml4
-rw-r--r--app/views/admin/users/_profile.html.haml4
-rw-r--r--app/views/admin/users/_projects.html.haml (renamed from app/views/users/_projects.html.haml)0
-rw-r--r--app/views/admin/users/edit.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml14
-rw-r--r--app/views/admin/users/projects.html.haml2
-rw-r--r--app/views/ci/admin/application_settings/_form.html.haml24
-rw-r--r--app/views/ci/admin/application_settings/show.html.haml3
-rw-r--r--app/views/ci/admin/builds/_build.html.haml34
-rw-r--r--app/views/ci/admin/builds/index.html.haml28
-rw-r--r--app/views/ci/admin/events/index.html.haml18
-rw-r--r--app/views/ci/admin/projects/_project.html.haml29
-rw-r--r--app/views/ci/admin/projects/index.html.haml16
-rw-r--r--app/views/ci/admin/runner_projects/index.html.haml57
-rw-r--r--app/views/ci/commits/_commit.html.haml5
-rw-r--r--app/views/ci/events/index.html.haml20
-rw-r--r--app/views/ci/lints/_create.html.haml9
-rw-r--r--app/views/ci/lints/create.js.haml2
-rw-r--r--app/views/ci/lints/show.html.haml34
-rw-r--r--app/views/ci/shared/_guide.html.haml8
-rw-r--r--app/views/ci/user_sessions/new.html.haml8
-rw-r--r--app/views/dashboard/_projects_head.html.haml30
-rw-r--r--app/views/dashboard/groups/index.html.haml6
-rw-r--r--app/views/dashboard/issues.html.haml20
-rw-r--r--app/views/dashboard/merge_requests.html.haml12
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml11
-rw-r--r--app/views/dashboard/milestones/index.html.haml16
-rw-r--r--app/views/dashboard/milestones/show.html.haml74
-rw-r--r--app/views/dashboard/projects/_projects.html.haml8
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/dashboard/snippets/index.html.haml26
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.erb7
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.haml10
-rw-r--r--app/views/devise/shared/_signup_box.html.haml12
-rw-r--r--app/views/devise/unlocks/new.html.erb12
-rw-r--r--app/views/devise/unlocks/new.html.haml14
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml2
-rw-r--r--app/views/explore/projects/index.html.haml4
-rw-r--r--app/views/explore/projects/starred.html.haml4
-rw-r--r--app/views/explore/projects/trending.html.haml4
-rw-r--r--app/views/explore/snippets/index.html.haml3
-rw-r--r--app/views/groups/_projects.html.haml4
-rw-r--r--app/views/groups/edit.html.haml6
-rw-r--r--app/views/groups/group_members/_group_member.html.haml12
-rw-r--r--app/views/groups/group_members/index.html.haml72
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/groups/issues.html.haml27
-rw-r--r--app/views/groups/merge_requests.html.haml19
-rw-r--r--app/views/groups/milestones/_milestone.html.haml2
-rw-r--r--app/views/groups/milestones/index.html.haml19
-rw-r--r--app/views/groups/milestones/new.html.haml47
-rw-r--r--app/views/groups/milestones/show.html.haml81
-rw-r--r--app/views/groups/new.html.haml8
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml78
-rw-r--r--app/views/help/_shortcuts.html.haml8
-rw-r--r--app/views/help/ui.html.haml48
-rw-r--r--app/views/import/bitbucket/status.html.haml5
-rw-r--r--app/views/import/fogbugz/new.html.haml1
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml7
-rw-r--r--app/views/import/fogbugz/status.html.haml5
-rw-r--r--app/views/import/github/status.html.haml5
-rw-r--r--app/views/import/gitlab/status.html.haml5
-rw-r--r--app/views/import/gitorious/status.html.haml5
-rw-r--r--app/views/import/google_code/new.html.haml3
-rw-r--r--app/views/import/google_code/new_user_map.html.haml17
-rw-r--r--app/views/import/google_code/status.html.haml5
-rw-r--r--app/views/layouts/_head.html.haml25
-rw-r--r--app/views/layouts/_page.html.haml10
-rw-r--r--app/views/layouts/_piwik.html.haml18
-rw-r--r--app/views/layouts/_search.html.haml6
-rw-r--r--app/views/layouts/admin.html.haml4
-rw-r--r--app/views/layouts/ci/_nav_admin.html.haml33
-rw-r--r--app/views/layouts/ci/_nav_project.html.haml12
-rw-r--r--app/views/layouts/ci/_page.html.haml10
-rw-r--r--app/views/layouts/ci/admin.html.haml11
-rw-r--r--app/views/layouts/ci/application.html.haml11
-rw-r--r--app/views/layouts/ci/project.html.haml11
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/errors.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml17
-rw-r--r--app/views/layouts/nav/_admin.html.haml43
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml22
-rw-r--r--app/views/layouts/nav/_explore.html.haml8
-rw-r--r--app/views/layouts/nav/_group.html.haml20
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml8
-rw-r--r--app/views/layouts/nav/_profile.html.haml26
-rw-r--r--app/views/layouts/nav/_project.html.haml58
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml42
-rw-r--r--app/views/layouts/notify.html.haml7
-rw-r--r--app/views/notify/build_fail_email.html.haml (renamed from app/views/ci/notify/build_fail_email.html.haml)10
-rw-r--r--app/views/notify/build_fail_email.text.erb (renamed from app/views/ci/notify/build_fail_email.text.erb)4
-rw-r--r--app/views/notify/build_success_email.html.haml (renamed from app/views/ci/notify/build_success_email.html.haml)10
-rw-r--r--app/views/notify/build_success_email.text.erb (renamed from app/views/ci/notify/build_success_email.text.erb)4
-rw-r--r--app/views/notify/project_was_moved_email.text.erb2
-rw-r--r--app/views/notify/repository_push_email.html.haml28
-rw-r--r--app/views/notify/repository_push_email.text.haml24
-rw-r--r--app/views/profiles/accounts/show.html.haml24
-rw-r--r--app/views/profiles/applications.html.haml84
-rw-r--r--app/views/profiles/keys/_form.html.haml7
-rw-r--r--app/views/profiles/keys/_key_table.html.haml16
-rw-r--r--app/views/profiles/keys/index.html.haml4
-rw-r--r--app/views/profiles/keys/new.html.haml4
-rw-r--r--app/views/profiles/notifications/_settings.html.haml2
-rw-r--r--app/views/profiles/notifications/show.html.haml6
-rw-r--r--app/views/profiles/preferences/show.html.haml2
-rw-r--r--app/views/profiles/show.html.haml10
-rw-r--r--app/views/profiles/two_factor_auths/new.html.haml1
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_commit_button.html.haml8
-rw-r--r--app/views/projects/_home_panel.html.haml48
-rw-r--r--app/views/projects/_last_commit.html.haml4
-rw-r--r--app/views/projects/_md_preview.html.haml19
-rw-r--r--app/views/projects/_readme.html.haml35
-rw-r--r--app/views/projects/_zen.html.haml9
-rw-r--r--app/views/projects/blame/show.html.haml3
-rw-r--r--app/views/projects/blob/_actions.html.haml15
-rw-r--r--app/views/projects/blob/_blob.html.haml6
-rw-r--r--app/views/projects/blob/_download.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml25
-rw-r--r--app/views/projects/blob/_new_dir.html.haml30
-rw-r--r--app/views/projects/blob/_remove.html.haml16
-rw-r--r--app/views/projects/blob/_upload.html.haml32
-rw-r--r--app/views/projects/blob/edit.html.haml19
-rw-r--r--app/views/projects/blob/new.html.haml17
-rw-r--r--app/views/projects/blob/show.html.haml6
-rw-r--r--app/views/projects/branches/_branch.html.haml37
-rw-r--r--app/views/projects/branches/_commit.html.haml4
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/branches/new.html.haml22
-rw-r--r--app/views/projects/builds/_build.html.haml53
-rw-r--r--app/views/projects/builds/_header_title.html.haml1
-rw-r--r--app/views/projects/builds/index.html.haml34
-rw-r--r--app/views/projects/builds/show.html.haml62
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml31
-rw-r--r--app/views/projects/buttons/_fork.html.haml30
-rw-r--r--app/views/projects/buttons/_notifications.html.haml4
-rw-r--r--app/views/projects/buttons/_star.html.haml22
-rw-r--r--app/views/projects/ci_services/_form.html.haml54
-rw-r--r--app/views/projects/ci_services/edit.html.haml1
-rw-r--r--app/views/projects/ci_services/index.html.haml22
-rw-r--r--app/views/projects/ci_settings/_form.html.haml119
-rw-r--r--app/views/projects/ci_settings/_no_runners.html.haml8
-rw-r--r--app/views/projects/ci_settings/edit.html.haml24
-rw-r--r--app/views/projects/ci_web_hooks/index.html.haml93
-rw-r--r--app/views/projects/commit/_builds.html.haml68
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml8
-rw-r--r--app/views/projects/commit/_commit_box.html.haml20
-rw-r--r--app/views/projects/commit/builds.html.haml6
-rw-r--r--app/views/projects/commit/ci.html.haml69
-rw-r--r--app/views/projects/commit/show.html.haml7
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml47
-rw-r--r--app/views/projects/commits/_commit.html.haml10
-rw-r--r--app/views/projects/commits/_head.html.haml7
-rw-r--r--app/views/projects/commits/show.atom.builder4
-rw-r--r--app/views/projects/compare/show.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_form.html.haml9
-rw-r--r--app/views/projects/deploy_keys/new.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml31
-rw-r--r--app/views/projects/edit.html.haml107
-rw-r--r--app/views/projects/empty.html.haml88
-rw-r--r--app/views/projects/forks/new.html.haml1
-rw-r--r--app/views/projects/graphs/_head.html.haml6
-rw-r--r--app/views/projects/graphs/ci.html.haml13
-rw-r--r--app/views/projects/graphs/ci/_build_times.haml17
-rw-r--r--app/views/projects/graphs/ci/_builds.haml50
-rw-r--r--app/views/projects/graphs/ci/_overall.haml27
-rw-r--r--app/views/projects/graphs/commits.html.haml60
-rw-r--r--app/views/projects/graphs/languages.html.haml32
-rw-r--r--app/views/projects/graphs/show.html.haml29
-rw-r--r--app/views/projects/hooks/index.html.haml11
-rw-r--r--app/views/projects/imports/new.html.haml25
-rw-r--r--app/views/projects/imports/show.html.haml2
-rw-r--r--app/views/projects/issues/_closed_by_box.html.haml5
-rw-r--r--app/views/projects/issues/_discussion.html.haml35
-rw-r--r--app/views/projects/issues/_form.html.haml8
-rw-r--r--app/views/projects/issues/_issue.html.haml43
-rw-r--r--app/views/projects/issues/_issues.html.haml7
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml26
-rw-r--r--app/views/projects/issues/edit.html.haml6
-rw-r--r--app/views/projects/issues/new.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml97
-rw-r--r--app/views/projects/issues/update.js.haml4
-rw-r--r--app/views/projects/labels/_form.html.haml10
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/labels/destroy.js.haml2
-rw-r--r--app/views/projects/labels/edit.html.haml8
-rw-r--r--app/views/projects/labels/index.html.haml7
-rw-r--r--app/views/projects/labels/new.html.haml6
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml31
-rw-r--r--app/views/projects/merge_requests/_form.html.haml3
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml67
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml6
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml31
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml30
-rw-r--r--app/views/projects/merge_requests/_show.html.haml82
-rw-r--r--app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml2
-rw-r--r--app/views/projects/merge_requests/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml5
-rw-r--r--app/views/projects/merge_requests/merge.js.haml8
-rw-r--r--app/views/projects/merge_requests/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_builds.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_commits.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml13
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml8
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml15
-rw-r--r--app/views/projects/merge_requests/show/_participants.html.haml4
-rw-r--r--app/views/projects/merge_requests/update.js.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml46
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml51
-rw-r--r--app/views/projects/merge_requests/widget/_open.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml71
-rw-r--r--app/views/projects/merge_requests/widget/open/_check.html.haml8
-rw-r--r--app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml26
-rw-r--r--app/views/projects/milestones/_form.html.haml16
-rw-r--r--app/views/projects/milestones/_milestone.html.haml8
-rw-r--r--app/views/projects/milestones/edit.html.haml6
-rw-r--r--app/views/projects/milestones/index.html.haml15
-rw-r--r--app/views/projects/milestones/new.html.haml6
-rw-r--r--app/views/projects/milestones/show.html.haml91
-rw-r--r--app/views/projects/network/_head.html.haml9
-rw-r--r--app/views/projects/network/show.html.haml3
-rw-r--r--app/views/projects/new.html.haml72
-rw-r--r--app/views/projects/notes/_edit_form.html.haml5
-rw-r--r--app/views/projects/notes/_form.html.haml11
-rw-r--r--app/views/projects/notes/_note.html.haml27
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml4
-rw-r--r--app/views/projects/project_members/_group_members.html.haml13
-rw-r--r--app/views/projects/project_members/_project_member.html.haml15
-rw-r--r--app/views/projects/project_members/_team.html.haml20
-rw-r--r--app/views/projects/project_members/index.html.haml44
-rw-r--r--app/views/projects/project_members/update.js.haml3
-rw-r--r--app/views/projects/protected_branches/index.html.haml5
-rw-r--r--app/views/projects/releases/edit.html.haml19
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml4
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-rw-r--r--app/views/projects/runners/_runner.html.haml6
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml10
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml2
-rw-r--r--app/views/projects/runners/edit.html.haml6
-rw-r--r--app/views/projects/runners/index.html.haml1
-rw-r--r--app/views/projects/runners/show.html.haml21
-rw-r--r--app/views/projects/services/_form.html.haml7
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/show.html.haml15
-rw-r--r--app/views/projects/snippets/_actions.html.haml11
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/index.html.haml20
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml46
-rw-r--r--app/views/projects/tags/_download.html.haml17
-rw-r--r--app/views/projects/tags/_tag.html.haml24
-rw-r--r--app/views/projects/tags/destroy.js.haml3
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/new.html.haml35
-rw-r--r--app/views/projects/tags/show.html.haml39
-rw-r--r--app/views/projects/tree/_tree_content.html.haml6
-rw-r--r--app/views/projects/tree/_tree_header.html.haml77
-rw-r--r--app/views/projects/triggers/index.html.haml9
-rw-r--r--app/views/projects/variables/show.html.haml7
-rw-r--r--app/views/projects/wikis/_form.html.haml20
-rw-r--r--app/views/projects/wikis/_main_links.html.haml11
-rw-r--r--app/views/projects/wikis/_nav.html.haml25
-rw-r--r--app/views/projects/wikis/_new.html.haml4
-rw-r--r--app/views/projects/wikis/edit.html.haml24
-rw-r--r--app/views/projects/wikis/git_access.html.haml2
-rw-r--r--app/views/projects/wikis/pages.html.haml7
-rw-r--r--app/views/projects/wikis/show.html.haml12
-rw-r--r--app/views/search/_category.html.haml7
-rw-r--r--app/views/search/results/_commit.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml50
-rw-r--r--app/views/shared/_commit_message_container.html.haml8
-rw-r--r--app/views/shared/_confirm_modal.html.haml7
-rw-r--r--app/views/shared/_file_highlight.html.haml5
-rw-r--r--app/views/shared/_group_form.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml16
-rw-r--r--app/views/shared/_issues.html.haml7
-rw-r--r--app/views/shared/_logo.svg14
-rw-r--r--app/views/shared/_merge_requests.html.haml7
-rw-r--r--app/views/shared/_milestone_expired.html.haml5
-rw-r--r--app/views/shared/_new_commit_form.html.haml22
-rw-r--r--app/views/shared/_new_project_item_select.html.haml20
-rw-r--r--app/views/shared/_project_limit.html.haml8
-rw-r--r--app/views/shared/_service_settings.html.haml9
-rw-r--r--app/views/shared/issuable/_context.html.haml50
-rw-r--r--app/views/shared/issuable/_filter.html.haml32
-rw-r--r--app/views/shared/issuable/_form.html.haml63
-rw-r--r--app/views/shared/issuable/_participants.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml83
-rw-r--r--app/views/shared/projects/_list.html.haml4
-rw-r--r--app/views/shared/projects/_project.html.haml6
-rw-r--r--app/views/shared/snippets/_form.html.haml7
-rw-r--r--app/views/shared/snippets/_header.html.haml25
-rw-r--r--app/views/shared/snippets/_snippet.html.haml1
-rw-r--r--app/views/sherlock/file_samples/show.html.haml55
-rw-r--r--app/views/sherlock/queries/_backtrace.html.haml27
-rw-r--r--app/views/sherlock/queries/_general.html.haml50
-rw-r--r--app/views/sherlock/queries/show.html.haml26
-rw-r--r--app/views/sherlock/transactions/_file_samples.html.haml24
-rw-r--r--app/views/sherlock/transactions/_general.html.haml39
-rw-r--r--app/views/sherlock/transactions/_queries.html.haml24
-rw-r--r--app/views/sherlock/transactions/index.html.haml42
-rw-r--r--app/views/sherlock/transactions/show.html.haml36
-rw-r--r--app/views/snippets/_actions.html.haml11
-rw-r--r--app/views/snippets/edit.html.haml2
-rw-r--r--app/views/snippets/new.html.haml2
-rw-r--r--app/views/snippets/show.html.haml51
-rw-r--r--app/views/users/show.atom.builder2
-rw-r--r--app/views/users/show.html.haml98
-rw-r--r--app/views/votes/_votes_block.html.haml56
-rw-r--r--app/views/votes/_votes_inline.html.haml9
-rw-r--r--app/workers/build_email_worker.rb19
-rw-r--r--app/workers/ci/hip_chat_notifier_worker.rb19
-rw-r--r--app/workers/ci/slack_notifier_worker.rb10
-rw-r--r--app/workers/ci/web_hook_worker.rb9
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/emails_on_push_worker.rb2
-rw-r--r--app/workers/merge_worker.rb13
-rw-r--r--app/workers/repository_fork_worker.rb14
-rw-r--r--app/workers/repository_import_worker.rb62
-rw-r--r--app/workers/stuck_ci_builds_worker.rb18
-rwxr-xr-xbin/background_jobs2
-rwxr-xr-x[-rw-r--r--]bin/ci/upgrade.rb0
-rwxr-xr-xbin/parallel-rsync-repos54
-rwxr-xr-xbin/rails6
-rwxr-xr-xbin/rake9
-rwxr-xr-xbin/setup29
-rwxr-xr-x[-rw-r--r--]bin/upgrade.rb0
-rw-r--r--config/application.rb4
-rw-r--r--config/database.yml.env9
-rw-r--r--config/environment.rb2
-rw-r--r--config/environments/development.rb2
-rw-r--r--config/environments/production.rb6
-rw-r--r--config/environments/test.rb10
-rw-r--r--config/gitlab.yml.example74
-rw-r--r--config/initializers/1_settings.rb80
-rw-r--r--config/initializers/2_app.rb6
-rw-r--r--config/initializers/4_ci_app.rb2
-rw-r--r--config/initializers/carrierwave.rb10
-rw-r--r--config/initializers/cookies_serializer.rb2
-rw-r--r--config/initializers/default_url_options.rb2
-rw-r--r--config/initializers/devise.rb16
-rw-r--r--config/initializers/inflections.rb21
-rw-r--r--config/initializers/metrics.rb63
-rw-r--r--config/initializers/omniauth.rb2
-rw-r--r--config/initializers/rack_attack.rb.example14
-rw-r--r--config/initializers/rack_lineprof.rb2
-rw-r--r--config/initializers/rack_profiler.rb10
-rw-r--r--config/initializers/secret_token.rb8
-rw-r--r--config/initializers/session_store.rb27
-rw-r--r--config/initializers/sherlock.rb5
-rw-r--r--config/initializers/sidekiq.rb15
-rw-r--r--config/initializers/smtp_settings.rb.sample2
-rw-r--r--config/initializers/state_machine_patch.rb9
-rw-r--r--config/initializers/static_files.rb4
-rw-r--r--config/locales/devise.en.yml1
-rw-r--r--config/locales/sherlock.en.yml38
-rw-r--r--config/routes.rb125
-rw-r--r--config/schedule.rb8
-rw-r--r--db/migrate/20121220064453_init_schema.rb74
-rw-r--r--db/migrate/20140122112253_create_merge_request_diffs.rb15
-rw-r--r--db/migrate/20140903115954_migrate_to_new_shell.rb2
-rw-r--r--db/migrate/20151012173029_set_jira_service_api_url.rb50
-rw-r--r--db/migrate/20151013092124_add_artifacts_file_to_builds.rb5
-rw-r--r--db/migrate/20151019111551_fix_build_tags.rb6
-rw-r--r--db/migrate/20151019111703_fail_build_without_names.rb5
-rw-r--r--db/migrate/20151020145526_add_services_template_index.rb5
-rw-r--r--db/migrate/20151023112551_fail_build_with_empty_name.rb8
-rw-r--r--db/migrate/20151023144219_remove_satellites.rb17
-rw-r--r--db/migrate/20151026182941_add_project_path_index.rb9
-rw-r--r--db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb7
-rw-r--r--db/migrate/20151103001141_add_public_to_group.rb5
-rw-r--r--db/migrate/20151103133339_add_shared_runners_setting.rb5
-rw-r--r--db/migrate/20151103134857_create_lfs_objects.rb10
-rw-r--r--db/migrate/20151103134958_create_lfs_objects_projects.rb12
-rw-r--r--db/migrate/20151104105513_add_file_to_lfs_objects.rb5
-rw-r--r--db/migrate/20151105094515_create_releases.rb14
-rw-r--r--db/migrate/20151106000015_add_is_award_to_notes.rb6
-rw-r--r--db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb5
-rw-r--r--db/migrate/20151109134526_add_issues_state_index.rb5
-rw-r--r--db/migrate/20151109134916_add_projects_visibility_level_index.rb5
-rw-r--r--db/migrate/20151110125604_add_import_error_to_project.rb5
-rw-r--r--db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb6
-rw-r--r--db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb7
-rw-r--r--db/migrate/20151118162244_add_projects_public_index.rb5
-rw-r--r--db/migrate/20151203162133_add_hide_project_limit_to_users.rb5
-rw-r--r--db/migrate/20151203162134_add_build_events_to_services.rb6
-rw-r--r--db/migrate/20151209144329_migrate_ci_web_hooks.rb16
-rw-r--r--db/migrate/20151209145909_migrate_ci_emails.rb45
-rw-r--r--db/migrate/20151210030143_add_unlock_token_to_user.rb5
-rw-r--r--db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb5
-rw-r--r--db/migrate/20151210125232_migrate_ci_slack_service.rb33
-rw-r--r--db/migrate/20151210125927_migrate_ci_hip_chat_service.rb34
-rw-r--r--db/migrate/20151210125928_add_ci_to_project.rb11
-rw-r--r--db/migrate/20151210125929_add_project_id_to_ci.rb8
-rw-r--r--db/migrate/20151210125930_migrate_ci_to_project.rb42
-rw-r--r--db/migrate/20151210125931_add_index_to_ci_tables.rb12
-rw-r--r--db/migrate/20151210125932_drop_null_for_ci_tables.rb9
-rw-r--r--db/migrate/20151218154042_add_tfa_to_application_settings.rb8
-rw-r--r--db/migrate/20151221234414_add_tfa_additional_fields.rb7
-rw-r--r--db/migrate/20151224123230_rename_emojis.rb15
-rw-r--r--db/migrate/20151228111122_remove_public_from_namespace.rb6
-rw-r--r--db/migrate/20151228150906_influxdb_settings.rb18
-rw-r--r--db/migrate/20151228175719_add_recaptcha_to_application_settings.rb9
-rw-r--r--db/migrate/20151229102248_influxdb_udp_port_setting.rb5
-rw-r--r--db/migrate/20151229112614_influxdb_remote_database_setting.rb5
-rw-r--r--db/schema.rb673
-rw-r--r--doc/README.md45
-rw-r--r--doc/administration/environment_variables.md53
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/commits.md10
-rw-r--r--doc/api/groups.md70
-rw-r--r--doc/api/merge_requests.md98
-rw-r--r--doc/api/notes.md17
-rw-r--r--doc/api/projects.md109
-rw-r--r--doc/api/repositories.md74
-rw-r--r--doc/api/repository_files.md3
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/api/tags.md131
-rw-r--r--doc/api/users.md19
-rw-r--r--doc/ci/README.md32
-rw-r--r--doc/ci/api/README.md2
-rw-r--r--doc/ci/api/projects.md22
-rw-r--r--doc/ci/api/runners.md4
-rw-r--r--doc/ci/docker/using_docker_build.md4
-rw-r--r--doc/ci/docker/using_docker_images.md302
-rw-r--r--doc/ci/examples/README.md6
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md2
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md4
-rw-r--r--doc/ci/examples/test-clojure-application.md4
-rw-r--r--doc/ci/img/builds_tab.pngbin0 -> 3845 bytes
-rw-r--r--doc/ci/languages/README.md7
-rw-r--r--doc/ci/languages/php.md284
-rw-r--r--doc/ci/quick_start/README.md204
-rw-r--r--doc/ci/quick_start/build_status.pngbin62140 -> 0 bytes
-rw-r--r--doc/ci/quick_start/commit_status.pngbin33492 -> 0 bytes
-rw-r--r--doc/ci/quick_start/img/build_log.pngbin0 -> 63272 bytes
-rw-r--r--doc/ci/quick_start/img/builds_status.pngbin0 -> 49121 bytes
-rw-r--r--doc/ci/quick_start/img/new_commit.pngbin0 -> 9033 bytes
-rw-r--r--doc/ci/quick_start/img/runners_activated.pngbin0 -> 27597 bytes
-rw-r--r--doc/ci/quick_start/img/single_commit_status_pending.pngbin0 -> 36431 bytes
-rw-r--r--doc/ci/quick_start/img/status_pending.pngbin0 -> 19782 bytes
-rw-r--r--doc/ci/quick_start/new_commit.pngbin47527 -> 0 bytes
-rw-r--r--doc/ci/quick_start/projects.pngbin37014 -> 0 bytes
-rw-r--r--doc/ci/quick_start/runners.pngbin123048 -> 0 bytes
-rw-r--r--doc/ci/quick_start/runners_activated.pngbin60769 -> 0 bytes
-rw-r--r--doc/ci/services/README.md9
-rw-r--r--doc/ci/services/docker-services.md5
-rw-r--r--doc/ci/services/mysql.md118
-rw-r--r--doc/ci/services/postgres.md114
-rw-r--r--doc/ci/services/redis.md69
-rw-r--r--doc/ci/ssh_keys/README.md109
-rw-r--r--doc/ci/triggers/README.md172
-rw-r--r--doc/ci/triggers/img/builds_page.pngbin0 -> 39713 bytes
-rw-r--r--doc/ci/triggers/img/trigger_single_build.pngbin0 -> 2895 bytes
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin0 -> 5418 bytes
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin0 -> 15889 bytes
-rw-r--r--doc/ci/variables/README.md2
-rw-r--r--doc/ci/yaml/README.md325
-rw-r--r--doc/customization/issue_closing.md27
-rw-r--r--doc/customization/libravatar.md6
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/development/db_dump.md2
-rw-r--r--doc/development/profiling.md45
-rw-r--r--doc/development/rake_tasks.md2
-rw-r--r--doc/development/shared_files.md33
-rw-r--r--doc/development/shell_commands.md20
-rw-r--r--doc/hooks/custom_hooks.md2
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/installation.md110
-rw-r--r--doc/install/requirements.md6
-rw-r--r--doc/integration/README.md3
-rw-r--r--doc/integration/bitbucket.md2
-rw-r--r--doc/integration/cas.md62
-rw-r--r--doc/integration/crowd.md2
-rw-r--r--doc/integration/facebook.md97
-rw-r--r--doc/integration/facebook_api_keys.pngbin0 -> 125921 bytes
-rw-r--r--doc/integration/facebook_app_settings.pngbin0 -> 134387 bytes
-rw-r--r--doc/integration/facebook_website_url.pngbin0 -> 42292 bytes
-rw-r--r--doc/integration/github.md2
-rw-r--r--doc/integration/gitlab.md2
-rw-r--r--doc/integration/google.md2
-rw-r--r--doc/integration/jira.md113
-rw-r--r--doc/integration/jira_issue_reference.pngbin0 -> 39942 bytes
-rw-r--r--doc/integration/jira_project_name.pngbin0 -> 60598 bytes
-rw-r--r--doc/integration/jira_service.pngbin0 -> 59082 bytes
-rw-r--r--doc/integration/jira_service_close_issue.pngbin0 -> 88433 bytes
-rw-r--r--doc/integration/jira_service_page.pngbin0 -> 162449 bytes
-rw-r--r--doc/integration/jira_workflow_screenshot.pngbin0 -> 121534 bytes
-rw-r--r--doc/integration/ldap.md12
-rw-r--r--doc/integration/omniauth.md5
-rw-r--r--doc/integration/recaptcha.md23
-rw-r--r--doc/integration/saml.md7
-rw-r--r--doc/integration/twitter.md2
-rw-r--r--doc/legal/corporate_contributor_license_agreement.md2
-rw-r--r--doc/legal/individual_contributor_license_agreement.md2
-rw-r--r--doc/markdown/markdown.md12
-rw-r--r--doc/operations/moving_repositories.md180
-rw-r--r--doc/operations/unicorn.md6
-rw-r--r--doc/permissions/permissions.md5
-rw-r--r--doc/public_access/public_access.md51
-rw-r--r--doc/raketasks/README.md3
-rw-r--r--doc/raketasks/backup_restore.md53
-rw-r--r--doc/raketasks/check.md63
-rw-r--r--doc/raketasks/check_repos_output.pngbin0 -> 73786 bytes
-rw-r--r--doc/raketasks/list_repos.md30
-rw-r--r--doc/release/README.md6
-rw-r--r--doc/release/monthly.md94
-rw-r--r--doc/release/patch.md40
-rw-r--r--doc/release/security.md4
-rw-r--r--doc/security/README.md5
-rw-r--r--doc/security/crime_vulnerability.md63
-rw-r--r--doc/security/two_factor_authentication.md38
-rw-r--r--doc/security/user_file_uploads.md11
-rw-r--r--doc/ssh/README.md19
-rw-r--r--doc/system_hooks/system_hooks.md50
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md2
-rw-r--r--doc/update/8.1-to-8.2.md188
-rw-r--r--doc/update/8.2-to-8.3.md200
-rw-r--r--doc/update/mysql_to_postgresql.md4
-rw-r--r--doc/update/patch_versions.md45
-rw-r--r--doc/web_hooks/web_hooks.md13
-rw-r--r--doc/workflow/README.md5
-rw-r--r--doc/workflow/award_emoji.pngbin0 -> 6620 bytes
-rw-r--r--doc/workflow/gitlab_flow.md23
-rw-r--r--doc/workflow/importing/README.md20
-rw-r--r--doc/workflow/importing/import_projects_from_github.md4
-rw-r--r--doc/workflow/importing/import_projects_from_gitlab_com.md4
-rw-r--r--doc/workflow/importing/migrating_from_svn.md79
-rw-r--r--doc/workflow/lfs/lfs_administration.md41
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md126
-rw-r--r--doc/workflow/merge_requests.md12
-rw-r--r--doc/workflow/merge_requests/commit_compare.pngbin0 -> 89631 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff.pngbin0 -> 120422 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff_without_whitespace.pngbin0 -> 98887 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds.md15
-rw-r--r--doc/workflow/merge_when_build_succeeds/enable.pngbin0 -> 151112 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds/status.pngbin0 -> 180318 bytes
-rw-r--r--doc/workflow/milestones.md13
-rw-r--r--doc/workflow/milestones/form.pngbin0 -> 88591 bytes
-rw-r--r--doc/workflow/milestones/group_form.pngbin0 -> 77087 bytes
-rw-r--r--doc/workflow/releases.md20
-rw-r--r--doc/workflow/releases/new_tag.pngbin0 -> 154755 bytes
-rw-r--r--doc/workflow/releases/tags.pngbin0 -> 165449 bytes
-rw-r--r--doc/workflow/voting_slider.pngbin5329 -> 0 bytes
-rw-r--r--doc_styleguide.md2
-rw-r--r--features/admin/groups.feature16
-rw-r--r--features/admin/projects.feature16
-rw-r--r--features/explore/groups.feature15
-rw-r--r--features/explore/projects.feature21
-rw-r--r--features/group/members.feature105
-rw-r--r--features/group/milestones.feature30
-rw-r--r--features/groups.feature112
-rw-r--r--features/profile/profile.feature1
-rw-r--r--features/project/active_tab.feature11
-rw-r--r--features/project/commits/branches.feature2
-rw-r--r--features/project/commits/comments.feature1
-rw-r--r--features/project/commits/commits.feature4
-rw-r--r--features/project/commits/diff_comments.feature7
-rw-r--r--features/project/commits/tags.feature21
-rw-r--r--features/project/commits/user_lookup.feature1
-rw-r--r--features/project/create.feature3
-rw-r--r--features/project/fork.feature11
-rw-r--r--features/project/graph.feature5
-rw-r--r--features/project/issues/award_emoji.feature30
-rw-r--r--features/project/issues/filter_labels.feature1
-rw-r--r--features/project/issues/issues.feature6
-rw-r--r--features/project/issues/labels.feature1
-rw-r--r--features/project/issues/milestones.feature1
-rw-r--r--features/project/merge_requests.feature36
-rw-r--r--features/project/merge_requests/accept.feature28
-rw-r--r--features/project/service.feature12
-rw-r--r--features/project/shortcuts.feature3
-rw-r--r--features/project/snippets.feature2
-rw-r--r--features/project/source/browse_files.feature151
-rw-r--r--features/project/star.feature1
-rw-r--r--features/project/team_management.feature6
-rw-r--r--features/snippets/snippets.feature2
-rw-r--r--features/steps/admin/groups.rb29
-rw-r--r--features/steps/admin/labels.rb12
-rw-r--r--features/steps/admin/projects.rb37
-rw-r--r--features/steps/admin/settings.rb2
-rw-r--r--features/steps/dashboard/dashboard.rb2
-rw-r--r--features/steps/dashboard/new_project.rb1
-rw-r--r--features/steps/explore/groups.rb8
-rw-r--r--features/steps/explore/projects.rb13
-rw-r--r--features/steps/group/members.rb147
-rw-r--r--features/steps/group/milestones.rb90
-rw-r--r--features/steps/groups.rb223
-rw-r--r--features/steps/profile/profile.rb17
-rw-r--r--features/steps/project/commits/branches.rb3
-rw-r--r--features/steps/project/commits/commits.rb8
-rw-r--r--features/steps/project/commits/tags.rb36
-rw-r--r--features/steps/project/create.rb7
-rw-r--r--features/steps/project/fork.rb19
-rw-r--r--features/steps/project/forked_merge_requests.rb10
-rw-r--r--features/steps/project/graph.rb15
-rw-r--r--features/steps/project/hooks.rb4
-rw-r--r--features/steps/project/issues/award_emoji.rb69
-rw-r--r--features/steps/project/issues/issues.rb25
-rw-r--r--features/steps/project/issues/labels.rb20
-rw-r--r--features/steps/project/issues/milestones.rb2
-rw-r--r--features/steps/project/merge_requests.rb83
-rw-r--r--features/steps/project/merge_requests/acceptance.rb43
-rw-r--r--features/steps/project/project.rb6
-rw-r--r--features/steps/project/services.rb28
-rw-r--r--features/steps/project/snippets.rb8
-rw-r--r--features/steps/project/source/browse_files.rb121
-rw-r--r--features/steps/project/source/markdown_render.rb6
-rw-r--r--features/steps/project/star.rb2
-rw-r--r--features/steps/project/team_management.rb4
-rw-r--r--features/steps/project/wiki.rb14
-rw-r--r--features/steps/shared/diff_note.rb19
-rw-r--r--features/steps/shared/group.rb8
-rw-r--r--features/steps/shared/paths.rb12
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--features/steps/shared/project_tab.rb10
-rw-r--r--features/steps/shared/user.rb12
-rw-r--r--features/steps/snippets/snippets.rb6
-rw-r--r--features/steps/snippets/user.rb6
-rw-r--r--features/support/capybara.rb2
-rw-r--r--fixtures/emojis/aliases.json367
-rw-r--r--fixtures/emojis/index.json13376
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/entities.rb74
-rw-r--r--lib/api/files.rb6
-rw-r--r--lib/api/groups.rb12
-rw-r--r--lib/api/helpers.rb57
-rw-r--r--lib/api/merge_requests.rb80
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/projects.rb37
-rw-r--r--lib/api/repositories.rb35
-rw-r--r--lib/api/tags.rb86
-rw-r--r--lib/api/triggers.rb48
-rw-r--r--lib/api/users.rb16
-rw-r--r--lib/award_emoji.rb51
-rw-r--r--lib/backup/artifacts.rb13
-rw-r--r--lib/backup/builds.rb33
-rw-r--r--lib/backup/database.rb54
-rw-r--r--lib/backup/files.rb40
-rw-r--r--lib/backup/lfs.rb13
-rw-r--r--lib/backup/manager.rb14
-rw-r--r--lib/backup/repository.rb6
-rw-r--r--lib/backup/uploads.rb32
-rw-r--r--lib/banzai.rb13
-rw-r--r--lib/banzai/cross_project_reference.rb22
-rw-r--r--lib/banzai/filter.rb10
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb158
-rw-r--r--lib/banzai/filter/autolink_filter.rb (renamed from lib/gitlab/markdown/autolink_filter.rb)6
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb58
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb63
-rw-r--r--lib/banzai/filter/emoji_filter.rb (renamed from lib/gitlab/markdown/emoji_filter.rb)6
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb (renamed from lib/gitlab/markdown/external_issue_reference_filter.rb)40
-rw-r--r--lib/banzai/filter/external_link_filter.rb (renamed from lib/gitlab/markdown/external_link_filter.rb)10
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb23
-rw-r--r--lib/banzai/filter/label_reference_filter.rb (renamed from lib/gitlab/markdown/label_reference_filter.rb)27
-rw-r--r--lib/banzai/filter/markdown_filter.rb42
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb41
-rw-r--r--lib/banzai/filter/redactor_filter.rb (renamed from lib/gitlab/markdown/redactor_filter.rb)21
-rw-r--r--lib/banzai/filter/reference_filter.rb (renamed from lib/gitlab/markdown/reference_filter.rb)113
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb (renamed from lib/gitlab/markdown/reference_gatherer_filter.rb)23
-rw-r--r--lib/banzai/filter/relative_link_filter.rb (renamed from lib/gitlab/markdown/relative_link_filter.rb)12
-rw-r--r--lib/banzai/filter/sanitization_filter.rb (renamed from lib/gitlab/markdown/sanitization_filter.rb)31
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb25
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb (renamed from lib/gitlab/markdown/syntax_highlight_filter.rb)6
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb (renamed from lib/gitlab/markdown/table_of_contents_filter.rb)8
-rw-r--r--lib/banzai/filter/task_list_filter.rb (renamed from lib/gitlab/markdown/task_list_filter.rb)6
-rw-r--r--lib/banzai/filter/upload_link_filter.rb (renamed from lib/gitlab/markdown/upload_link_filter.rb)6
-rw-r--r--lib/banzai/filter/user_reference_filter.rb (renamed from lib/gitlab/markdown/user_reference_filter.rb)59
-rw-r--r--lib/banzai/lazy_reference.rb27
-rw-r--r--lib/banzai/pipeline.rb10
-rw-r--r--lib/banzai/pipeline/asciidoc_pipeline.rb13
-rw-r--r--lib/banzai/pipeline/atom_pipeline.rb14
-rw-r--r--lib/banzai/pipeline/base_pipeline.rb30
-rw-r--r--lib/banzai/pipeline/combined_pipeline.rb27
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb14
-rw-r--r--lib/banzai/pipeline/email_pipeline.rb13
-rw-r--r--lib/banzai/pipeline/full_pipeline.rb9
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb41
-rw-r--r--lib/banzai/pipeline/note_pipeline.rb14
-rw-r--r--lib/banzai/pipeline/plain_markdown_pipeline.rb13
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb20
-rw-r--r--lib/banzai/pipeline/reference_extraction_pipeline.rb13
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb9
-rw-r--r--lib/banzai/querying.rb18
-rw-r--r--lib/banzai/reference_extractor.rb55
-rw-r--r--lib/banzai/renderer.rb85
-rw-r--r--lib/ci/api/api.rb5
-rw-r--r--lib/ci/api/builds.rb102
-rw-r--r--lib/ci/api/commits.rb66
-rw-r--r--lib/ci/api/entities.rb16
-rw-r--r--lib/ci/api/helpers.rb21
-rw-r--r--lib/ci/api/projects.rb195
-rw-r--r--lib/ci/api/runners.rb15
-rw-r--r--lib/ci/api/triggers.rb2
-rw-r--r--lib/ci/charts.rb3
-rw-r--r--lib/ci/current_settings.rb22
-rw-r--r--lib/ci/git.rb5
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb175
-rw-r--r--lib/ci/scheduler.rb16
-rw-r--r--lib/file_streamer.rb16
-rw-r--r--lib/gitlab/asciidoc.rb27
-rw-r--r--lib/gitlab/backend/grack_auth.rb20
-rw-r--r--lib/gitlab/backend/shell.rb9
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb3
-rw-r--r--lib/gitlab/blacklist.rb34
-rw-r--r--lib/gitlab/build_data_builder.rb64
-rw-r--r--lib/gitlab/closing_issue_extractor.rb16
-rw-r--r--lib/gitlab/compare_result.rb4
-rw-r--r--lib/gitlab/contributions_calendar.rb4
-rw-r--r--lib/gitlab/current_settings.rb8
-rw-r--r--lib/gitlab/database.rb20
-rw-r--r--lib/gitlab/diff/file.rb4
-rw-r--r--lib/gitlab/email/message/repository_push.rb137
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb2
-rw-r--r--lib/gitlab/fogbugz_import/project_creator.rb3
-rw-r--r--lib/gitlab/force_push_check.rb2
-rw-r--r--lib/gitlab/git.rb4
-rw-r--r--lib/gitlab/git/hook.rb17
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/git_ref_validator.rb2
-rw-r--r--lib/gitlab/github_import/base_formatter.rb21
-rw-r--r--lib/gitlab/github_import/client.rb2
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb45
-rw-r--r--lib/gitlab/github_import/importer.rb64
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb66
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb101
-rw-r--r--lib/gitlab/gitlab_import/client.rb2
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb3
-rw-r--r--lib/gitlab/gitorious_import/project_creator.rb3
-rw-r--r--lib/gitlab/google_code_import/importer.rb99
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb3
-rw-r--r--lib/gitlab/inline_diff.rb87
-rw-r--r--lib/gitlab/ldap/access.rb4
-rw-r--r--lib/gitlab/ldap/user.rb6
-rw-r--r--lib/gitlab/lfs/response.rb327
-rw-r--r--lib/gitlab/lfs/router.rb97
-rw-r--r--lib/gitlab/markdown.rb200
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb92
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb88
-rw-r--r--lib/gitlab/markdown/cross_project_reference.rb24
-rw-r--r--lib/gitlab/markdown/issue_reference_filter.rb72
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb74
-rw-r--r--lib/gitlab/markdown/pipeline.rb34
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb74
-rw-r--r--lib/gitlab/metrics.rb102
-rw-r--r--lib/gitlab/metrics/delta.rb32
-rw-r--r--lib/gitlab/metrics/instrumentation.rb148
-rw-r--r--lib/gitlab/metrics/metric.rb28
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb49
-rw-r--r--lib/gitlab/metrics/sampler.rb107
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb23
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb54
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb22
-rw-r--r--lib/gitlab/metrics/system.rb35
-rw-r--r--lib/gitlab/metrics/transaction.rb79
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb2
-rw-r--r--lib/gitlab/o_auth/provider.rb9
-rw-r--r--lib/gitlab/o_auth/session.rb17
-rw-r--r--lib/gitlab/o_auth/user.rb2
-rw-r--r--lib/gitlab/project_search_results.rb18
-rw-r--r--lib/gitlab/push_data_builder.rb5
-rw-r--r--lib/gitlab/recaptcha.rb14
-rw-r--r--lib/gitlab/reference_extractor.rb73
-rw-r--r--lib/gitlab/regex.rb17
-rw-r--r--lib/gitlab/seeder.rb2
-rw-r--r--lib/gitlab/sherlock.rb19
-rw-r--r--lib/gitlab/sherlock/collection.rb49
-rw-r--r--lib/gitlab/sherlock/file_sample.rb31
-rw-r--r--lib/gitlab/sherlock/line_profiler.rb98
-rw-r--r--lib/gitlab/sherlock/line_sample.rb36
-rw-r--r--lib/gitlab/sherlock/location.rb26
-rw-r--r--lib/gitlab/sherlock/middleware.rb41
-rw-r--r--lib/gitlab/sherlock/query.rb114
-rw-r--r--lib/gitlab/sherlock/transaction.rb136
-rw-r--r--lib/gitlab/sql/union.rb34
-rw-r--r--lib/gitlab/upgrader.rb8
-rw-r--r--lib/gitlab/visibility_level.rb9
-rw-r--r--lib/omni_auth/request_forgery_protection.rb63
-rw-r--r--lib/rouge/formatters/html_gitlab.rb2
-rwxr-xr-xlib/support/init.d/gitlab81
-rwxr-xr-xlib/support/init.d/gitlab.default.example15
-rw-r--r--lib/support/nginx/gitlab129
-rw-r--r--lib/support/nginx/gitlab-ssl132
-rw-r--r--lib/tasks/ci/schedule_builds.rake6
-rw-r--r--lib/tasks/flay.rake9
-rw-r--r--lib/tasks/flog.rake25
-rw-r--r--lib/tasks/gitlab/backup.rake42
-rw-r--r--lib/tasks/gitlab/check.rake58
-rw-r--r--lib/tasks/gitlab/git.rake55
-rw-r--r--lib/tasks/gitlab/import.rake2
-rw-r--r--lib/tasks/gitlab/list_repos.rake17
-rw-r--r--lib/tasks/gitlab/shell.rake10
-rw-r--r--lib/tasks/gitlab/task_helpers.rake20
-rw-r--r--lib/tasks/grape.rake8
-rw-r--r--lib/tasks/spec.rake29
-rw-r--r--lib/tasks/spinach.rake20
-rw-r--r--lib/uploaded_file.rb37
-rw-r--r--lib/version_check.rb2
-rwxr-xr-xscripts/prepare_build.sh4
-rw-r--r--shared/.gitkeep0
-rw-r--r--shared/artifacts/.gitkeep0
-rw-r--r--shared/artifacts/tmp/cache/.gitkeep0
-rw-r--r--shared/artifacts/tmp/uploads/.gitkeep0
-rw-r--r--shared/lfs-objects/.gitkeep0
-rw-r--r--spec/benchmarks/finders/issues_finder_spec.rb55
-rw-r--r--spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb2
-rw-r--r--spec/benchmarks/models/user_spec.rb36
-rw-r--r--spec/benchmarks/services/projects/create_service_spec.rb28
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb76
-rw-r--r--spec/controllers/admin/impersonation_controller_spec.rb19
-rw-r--r--spec/controllers/admin/users_controller_spec.rb15
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb9
-rw-r--r--spec/controllers/commit_controller_spec.rb35
-rw-r--r--spec/controllers/groups/milestones_controller_spec.rb27
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb16
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb52
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb5
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb34
-rw-r--r--spec/controllers/projects/tree_controller_spec.rb8
-rw-r--r--spec/controllers/projects_controller_spec.rb51
-rw-r--r--spec/controllers/snippets_controller_spec.rb233
-rw-r--r--spec/controllers/users_controller_spec.rb23
-rw-r--r--spec/factories.rb15
-rw-r--r--spec/factories/ci/builds.rb4
-rw-r--r--spec/factories/ci/commits.rb25
-rw-r--r--spec/factories/ci/events.rb24
-rw-r--r--spec/factories/ci/projects.rb44
-rw-r--r--spec/factories/ci/runner_projects.rb2
-rw-r--r--spec/factories/ci/web_hook.rb6
-rw-r--r--spec/factories/commit_statuses.rb2
-rw-r--r--spec/factories/labels.rb1
-rw-r--r--spec/factories/lfs_objects.rb24
-rw-r--r--spec/factories/lfs_objects_projects.rb19
-rw-r--r--spec/factories/merge_requests.rb6
-rw-r--r--spec/factories/notes.rb1
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/factories/releases.rb21
-rw-r--r--spec/features/admin/admin_builds_spec.rb98
-rw-r--r--spec/features/admin/admin_runners_spec.rb (renamed from spec/features/ci/admin/runners_spec.rb)32
-rw-r--r--spec/features/admin/admin_users_spec.rb65
-rw-r--r--spec/features/builds_spec.rb68
-rw-r--r--spec/features/ci/admin/builds_spec.rb70
-rw-r--r--spec/features/ci/admin/events_spec.rb20
-rw-r--r--spec/features/ci/admin/projects_spec.rb19
-rw-r--r--spec/features/ci/events_spec.rb22
-rw-r--r--spec/features/ci/lint_spec.rb28
-rw-r--r--spec/features/ci_lint_spec.rb39
-rw-r--r--spec/features/ci_settings_spec.rb22
-rw-r--r--spec/features/ci_web_hooks_spec.rb27
-rw-r--r--spec/features/commits_spec.rb101
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb4
-rw-r--r--spec/features/issues/note_polling_spec.rb16
-rw-r--r--spec/features/issues_spec.rb35
-rw-r--r--spec/features/login_spec.rb52
-rw-r--r--spec/features/merge_requests/merge_when_build_succeeds_spec.rb85
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb5
-rw-r--r--spec/features/password_reset_spec.rb26
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/features/runners_spec.rb34
-rw-r--r--spec/features/security/group_access_spec.rb4
-rw-r--r--spec/features/task_lists_spec.rb4
-rw-r--r--spec/features/triggers_spec.rb7
-rw-r--r--spec/features/variables_spec.rb7
-rw-r--r--spec/finders/contributed_projects_finder_spec.rb35
-rw-r--r--spec/finders/personal_projects_finder_spec.rb34
-rw-r--r--spec/finders/projects_finder_spec.rb82
-rw-r--r--spec/fixtures/markdown.md.erb17
-rw-r--r--spec/helpers/application_helper_spec.rb25
-rw-r--r--spec/helpers/ci_status_helper_spec.rb11
-rw-r--r--spec/helpers/groups_helper.rb2
-rw-r--r--spec/helpers/issues_helper_spec.rb21
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb41
-rw-r--r--spec/helpers/page_layout_helper_spec.rb129
-rw-r--r--spec/helpers/projects_helper_spec.rb10
-rw-r--r--spec/helpers/search_helper_spec.rb5
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb85
-rw-r--r--spec/javascripts/fixtures/issues_show.html.haml14
-rw-r--r--spec/javascripts/fixtures/merge_request_tabs.html.haml6
-rw-r--r--spec/javascripts/fixtures/merge_requests_show.html.haml2
-rw-r--r--spec/javascripts/fixtures/new_branch.html.haml4
-rw-r--r--spec/javascripts/issue_spec.js.coffee86
-rw-r--r--spec/javascripts/new_branch_spec.js.coffee160
-rw-r--r--spec/lib/banzai/cross_project_reference_spec.rb34
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb112
-rw-r--r--spec/lib/banzai/filter/commit_range_reference_filter_spec.rb182
-rw-r--r--spec/lib/banzai/filter/commit_reference_filter_spec.rb163
-rw-r--r--spec/lib/banzai/filter/emoji_filter_spec.rb98
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb77
-rw-r--r--spec/lib/banzai/filter/external_link_filter_spec.rb29
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb209
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb179
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb142
-rw-r--r--spec/lib/banzai/filter/redactor_filter_spec.rb89
-rw-r--r--spec/lib/banzai/filter/reference_gatherer_filter_spec.rb87
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb155
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb197
-rw-r--r--spec/lib/banzai/filter/snippet_reference_filter_spec.rb146
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb17
-rw-r--r--spec/lib/banzai/filter/table_of_contents_filter_spec.rb97
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb10
-rw-r--r--spec/lib/banzai/filter/upload_link_filter_spec.rb73
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb160
-rw-r--r--spec/lib/banzai/querying_spec.rb13
-rw-r--r--spec/lib/ci/ansi2html_spec.rb2
-rw-r--r--spec/lib/ci/charts_spec.rb2
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb456
-rw-r--r--spec/lib/disable_email_interceptor_spec.rb2
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/lib/file_size_validator_spec.rb2
-rw-r--r--spec/lib/git_ref_validator_spec.rb2
-rw-r--r--spec/lib/gitlab/asciidoc_spec.rb8
-rw-r--r--spec/lib/gitlab/auth_spec.rb2
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb27
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb4
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/build_data_builder_spec.rb20
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb55
-rw-r--r--spec/lib/gitlab/color_schemes_spec.rb2
-rw-r--r--spec/lib/gitlab/database_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/email/attachment_uploader_spec.rb2
-rw-r--r--spec/lib/gitlab/email/message/repository_push_spec.rb122
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb2
-rw-r--r--spec/lib/gitlab/email/reply_parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb139
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb184
-rw-r--r--spec/lib/gitlab/gitlab_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/gitorious_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb2
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb2
-rw-r--r--spec/lib/gitlab/inline_diff_spec.rb (renamed from spec/lib/gitlab/diff/inline_diff_spec.rb)2
-rw-r--r--spec/lib/gitlab/key_fingerprint_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb7
-rw-r--r--spec/lib/gitlab/ldap/adapter_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/authentication_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb17
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb765
-rw-r--r--spec/lib/gitlab/markdown/autolink_filter_spec.rb114
-rw-r--r--spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb145
-rw-r--r--spec/lib/gitlab/markdown/commit_reference_filter_spec.rb135
-rw-r--r--spec/lib/gitlab/markdown/cross_project_reference_spec.rb36
-rw-r--r--spec/lib/gitlab/markdown/emoji_filter_spec.rb95
-rw-r--r--spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb79
-rw-r--r--spec/lib/gitlab/markdown/external_link_filter_spec.rb31
-rw-r--r--spec/lib/gitlab/markdown/issue_reference_filter_spec.rb139
-rw-r--r--spec/lib/gitlab/markdown/label_reference_filter_spec.rb144
-rw-r--r--spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb120
-rw-r--r--spec/lib/gitlab/markdown/redactor_filter_spec.rb91
-rw-r--r--spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb89
-rw-r--r--spec/lib/gitlab/markdown/relative_link_filter_spec.rb149
-rw-r--r--spec/lib/gitlab/markdown/sanitization_filter_spec.rb118
-rw-r--r--spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb118
-rw-r--r--spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb19
-rw-r--r--spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb99
-rw-r--r--spec/lib/gitlab/markdown/task_list_filter_spec.rb12
-rw-r--r--spec/lib/gitlab/markdown/upload_link_filter_spec.rb75
-rw-r--r--spec/lib/gitlab/markdown/user_reference_filter_spec.rb122
-rw-r--r--spec/lib/gitlab/markup_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/delta_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb240
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb51
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb63
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb119
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb26
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb40
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb35
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb29
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb89
-rw-r--r--spec/lib/gitlab/metrics_spec.rb72
-rw-r--r--spec/lib/gitlab/note_data_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/o_auth/auth_hash_spec.rb6
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb2
-rw-r--r--spec/lib/gitlab/popen_spec.rb2
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb5
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb12
-rw-r--r--spec/lib/gitlab/regex_spec.rb2
-rw-r--r--spec/lib/gitlab/sherlock/collection_spec.rb82
-rw-r--r--spec/lib/gitlab/sherlock/file_sample_spec.rb54
-rw-r--r--spec/lib/gitlab/sherlock/line_profiler_spec.rb73
-rw-r--r--spec/lib/gitlab/sherlock/line_sample_spec.rb33
-rw-r--r--spec/lib/gitlab/sherlock/location_spec.rb40
-rw-r--r--spec/lib/gitlab/sherlock/middleware_spec.rb79
-rw-r--r--spec/lib/gitlab/sherlock/query_spec.rb113
-rw-r--r--spec/lib/gitlab/sherlock/transaction_spec.rb235
-rw-r--r--spec/lib/gitlab/sql/union_spec.rb16
-rw-r--r--spec/lib/gitlab/themes_spec.rb2
-rw-r--r--spec/lib/gitlab/upgrader_spec.rb2
-rw-r--r--spec/lib/gitlab/uploads_transfer_spec.rb2
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb2
-rw-r--r--spec/lib/gitlab/version_info_spec.rb2
-rw-r--r--spec/lib/repository_cache_spec.rb2
-rw-r--r--spec/lib/votes_spec.rb188
-rw-r--r--spec/mailers/abuse_report_mailer_spec.rb38
-rw-r--r--spec/mailers/ci/notify_spec.rb35
-rw-r--r--spec/mailers/notify_spec.rb108
-rw-r--r--spec/models/abuse_report_spec.rb17
-rw-r--r--spec/models/application_setting_spec.rb27
-rw-r--r--spec/models/broadcast_message_spec.rb17
-rw-r--r--spec/models/build_spec.rb170
-rw-r--r--spec/models/ci/commit_spec.rb53
-rw-r--r--spec/models/ci/project_services/hip_chat_message_spec.rb39
-rw-r--r--spec/models/ci/project_services/hip_chat_service_spec.rb73
-rw-r--r--spec/models/ci/project_services/mail_service_spec.rb191
-rw-r--r--spec/models/ci/project_services/slack_message_spec.rb43
-rw-r--r--spec/models/ci/project_services/slack_service_spec.rb57
-rw-r--r--spec/models/ci/project_spec.rb258
-rw-r--r--spec/models/ci/runner_project_spec.rb4
-rw-r--r--spec/models/ci/runner_spec.rb12
-rw-r--r--spec/models/ci/service_spec.rb48
-rw-r--r--spec/models/ci/trigger_spec.rb16
-rw-r--r--spec/models/ci/variable_spec.rb4
-rw-r--r--spec/models/ci/web_hook_spec.rb63
-rw-r--r--spec/models/commit_range_spec.rb129
-rw-r--r--spec/models/commit_spec.rb34
-rw-r--r--spec/models/commit_status_spec.rb38
-rw-r--r--spec/models/concerns/case_sensitivity_spec.rb2
-rw-r--r--spec/models/concerns/issuable_spec.rb32
-rw-r--r--spec/models/concerns/mentionable_spec.rb17
-rw-r--r--spec/models/concerns/strip_attribute_spec.rb20
-rw-r--r--spec/models/concerns/token_authenticatable_spec.rb68
-rw-r--r--spec/models/deploy_key_spec.rb2
-rw-r--r--spec/models/deploy_keys_project_spec.rb2
-rw-r--r--spec/models/event_spec.rb40
-rw-r--r--spec/models/external_issue_spec.rb2
-rw-r--r--spec/models/external_wiki_service_spec.rb2
-rw-r--r--spec/models/generic_commit_status_spec.rb35
-rw-r--r--spec/models/global_milestone_spec.rb75
-rw-r--r--spec/models/group_spec.rb22
-rw-r--r--spec/models/hooks/project_hook_spec.rb2
-rw-r--r--spec/models/hooks/service_hook_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb2
-rw-r--r--spec/models/hooks/web_hook_spec.rb8
-rw-r--r--spec/models/issue_spec.rb2
-rw-r--r--spec/models/jira_issue_spec.rb30
-rw-r--r--spec/models/key_spec.rb4
-rw-r--r--spec/models/label_link_spec.rb2
-rw-r--r--spec/models/label_spec.rb3
-rw-r--r--spec/models/member_spec.rb2
-rw-r--r--spec/models/members/group_member_spec.rb2
-rw-r--r--spec/models/members/project_member_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb111
-rw-r--r--spec/models/milestone_spec.rb2
-rw-r--r--spec/models/namespace_spec.rb3
-rw-r--r--spec/models/note_spec.rb121
-rw-r--r--spec/models/project_security_spec.rb2
-rw-r--r--spec/models/project_services/asana_service_spec.rb77
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb2
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb2
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb2
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb2
-rw-r--r--spec/models/project_services/gitlab_ci_service_spec.rb57
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb2
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb73
-rw-r--r--spec/models/project_services/irker_service_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb126
-rw-r--r--spec/models/project_services/pushover_service_spec.rb2
-rw-r--r--spec/models/project_services/slack_service/build_message_spec.rb46
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb2
-rw-r--r--spec/models/project_services/slack_service/merge_message_spec.rb2
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb10
-rw-r--r--spec/models/project_services/slack_service/push_message_spec.rb2
-rw-r--r--spec/models/project_services/slack_service_spec.rb2
-rw-r--r--spec/models/project_snippet_spec.rb2
-rw-r--r--spec/models/project_spec.rb163
-rw-r--r--spec/models/project_team_spec.rb2
-rw-r--r--spec/models/project_wiki_spec.rb22
-rw-r--r--spec/models/protected_branch_spec.rb2
-rw-r--r--spec/models/release_spec.rb28
-rw-r--r--spec/models/repository_spec.rb130
-rw-r--r--spec/models/service_spec.rb2
-rw-r--r--spec/models/snippet_spec.rb2
-rw-r--r--spec/models/user_spec.rb74
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/requests/api/api_helpers_spec.rb26
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/requests/api/commit_status_spec.rb4
-rw-r--r--spec/requests/api/files_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb59
-rw-r--r--spec/requests/api/labels_spec.rb10
-rw-r--r--spec/requests/api/merge_requests_spec.rb36
-rw-r--r--spec/requests/api/project_hooks_spec.rb12
-rw-r--r--spec/requests/api/projects_spec.rb110
-rw-r--r--spec/requests/api/repositories_spec.rb75
-rw-r--r--spec/requests/api/services_spec.rb3
-rw-r--r--spec/requests/api/tags_spec.rb196
-rw-r--r--spec/requests/api/triggers_spec.rb80
-rw-r--r--spec/requests/api/users_spec.rb26
-rw-r--r--spec/requests/ci/api/builds_spec.rb214
-rw-r--r--spec/requests/ci/api/commits_spec.rb65
-rw-r--r--spec/requests/ci/api/projects_spec.rb266
-rw-r--r--spec/requests/ci/api/runners_spec.rb36
-rw-r--r--spec/requests/ci/api/triggers_spec.rb21
-rw-r--r--spec/services/archive_repository_service_spec.rb2
-rw-r--r--spec/services/ci/create_commit_service_spec.rb154
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb7
-rw-r--r--spec/services/ci/event_service_spec.rb34
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb9
-rw-r--r--spec/services/ci/register_build_service_spec.rb14
-rw-r--r--spec/services/ci/web_hook_service_spec.rb37
-rw-r--r--spec/services/create_commit_builds_service_spec.rb175
-rw-r--r--spec/services/create_release_service_spec.rb34
-rw-r--r--spec/services/create_snippet_service_spec.rb2
-rw-r--r--spec/services/destroy_group_service_spec.rb2
-rw-r--r--spec/services/event_create_service_spec.rb2
-rw-r--r--spec/services/git_hooks_service_spec.rb53
-rw-r--r--spec/services/git_push_service_spec.rb71
-rw-r--r--spec/services/git_tag_push_service_spec.rb18
-rw-r--r--spec/services/issues/bulk_update_service_spec.rb2
-rw-r--r--spec/services/issues/close_service_spec.rb6
-rw-r--r--spec/services/issues/create_service_spec.rb2
-rw-r--r--spec/services/issues/update_service_spec.rb102
-rw-r--r--spec/services/merge_requests/close_service_spec.rb6
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb12
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb84
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb27
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb6
-rw-r--r--spec/services/merge_requests/update_service_spec.rb70
-rw-r--r--spec/services/milestones/close_service_spec.rb28
-rw-r--r--spec/services/milestones/create_service_spec.rb24
-rw-r--r--spec/services/milestones/group_service_spec.rb70
-rw-r--r--spec/services/notes/create_service_spec.rb36
-rw-r--r--spec/services/notification_service_spec.rb368
-rw-r--r--spec/services/projects/create_service_spec.rb31
-rw-r--r--spec/services/projects/destroy_service_spec.rb2
-rw-r--r--spec/services/projects/download_service_spec.rb2
-rw-r--r--spec/services/projects/fork_service_spec.rb21
-rw-r--r--spec/services/projects/transfer_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb41
-rw-r--r--spec/services/projects/upload_service_spec.rb2
-rw-r--r--spec/services/search_service_spec.rb2
-rw-r--r--spec/services/system_hooks_service_spec.rb49
-rw-r--r--spec/services/system_note_service_spec.rb89
-rw-r--r--spec/services/test_hook_service_spec.rb2
-rw-r--r--spec/services/update_release_service_spec.rb34
-rw-r--r--spec/services/update_snippet_service_spec.rb4
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/support/filter_spec_helper.rb39
-rw-r--r--spec/support/jira_service_helper.rb67
-rw-r--r--spec/support/markdown_feature.rb4
-rw-r--r--spec/support/matchers/markdown_matchers.rb14
-rw-r--r--spec/support/mentionable_shared_examples.rb23
-rw-r--r--spec/support/repo_helpers.rb12
-rw-r--r--spec/support/stub_gitlab_calls.rb4
-rw-r--r--spec/support/test_env.rb12
-rw-r--r--spec/support/wait_for_ajax.rb11
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb32
-rw-r--r--spec/workers/build_email_worker_spec.rb35
-rw-r--r--spec/workers/email_receiver_worker_spec.rb14
-rw-r--r--spec/workers/repository_fork_worker_spec.rb21
-rw-r--r--spec/workers/stuck_ci_builds_worker_spec.rb44
-rw-r--r--vendor/assets/javascripts/clipboard.js621
-rw-r--r--vendor/assets/javascripts/jquery.blockUI.js590
-rw-r--r--vendor/assets/javascripts/jquery.history.js1
1577 files changed, 50296 insertions, 17600 deletions
diff --git a/.flayignore b/.flayignore
new file mode 100644
index 00000000000..9c9875d4f9e
--- /dev/null
+++ b/.flayignore
@@ -0,0 +1 @@
+*.erb
diff --git a/.gitignore b/.gitignore
index 73bde4cc761..91ea81bfc4e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,6 +26,7 @@ config/initializers/smtp_settings.rb
config/resque.yml
config/unicorn.rb
config/secrets.yml
+config/sidekiq.yml
coverage/*
db/*.sqlite3
db/*.sqlite3-journal
@@ -37,8 +38,10 @@ nohup.out
public/assets/
public/uploads.*
public/uploads/
+shared/artifacts/
rails_best_practices_output.html
/tags
tmp/
vendor/bundle/*
builds/*
+shared/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index cf6d28b01af..c23a7a3bf0e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -8,10 +8,11 @@ before_script:
- touch log/application.log
- touch log/test.log
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"
- - bundle exec rake db:create RAILS_ENV=test
+ - bundle exec rake db:reset db:create RAILS_ENV=test
spec:feature:
script:
+ - RAILS_ENV=test bundle exec rake assets:precompile 2>/dev/null
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:feature
tags:
- ruby
@@ -24,6 +25,27 @@ spec:api:
- ruby
- mysql
+spec:models:
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:models
+ tags:
+ - ruby
+ - mysql
+
+spec:lib:
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:lib
+ tags:
+ - ruby
+ - mysql
+
+spec:services:
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:services
+ tags:
+ - ruby
+ - mysql
+
spec:benchmark:
script:
- RAILS_ENV=test bundle exec rake spec:benchmark
@@ -39,9 +61,16 @@ spec:other:
- ruby
- mysql
-spinach:project:
+spinach:project:half:
script:
- - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:half
+ tags:
+ - ruby
+ - mysql
+
+spinach:project:rest:
+ script:
+ - RAILS_ENV=test SIMPLECOV=true bundle exec rake spinach:project:rest
tags:
- ruby
- mysql
@@ -73,3 +102,26 @@ brakeman:
tags:
- ruby
- mysql
+
+flog:
+ script:
+ - bundle exec rake flog
+ tags:
+ - ruby
+ - mysql
+
+flay:
+ script:
+ - bundle exec rake flay
+ tags:
+ - ruby
+ - mysql
+
+bundler:audit:
+ script:
+ - "bundle exec bundle-audit update"
+ - "bundle exec bundle-audit check"
+ tags:
+ - ruby
+ - mysql
+ allow_failure: true
diff --git a/.rubocop.yml b/.rubocop.yml
index 11e4502849a..89aa0591c31 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -76,7 +76,7 @@ Style/BlockEndNewline:
Description: 'Put end statement of multiline block on its own line.'
Enabled: true
-Style/Blocks:
+Style/BlockDelimiters:
Description: >-
Avoid using {...} for multi-line blocks (multiline chaining is
always ugly).
@@ -232,6 +232,10 @@ Style/EvenOdd:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
Enabled: false
+Style/ExtraSpacing:
+ Description: 'Do not use unnecessary spacing.'
+ Enabled: false
+
Style/FileName:
Description: 'Use snake_case for source file names.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#snake-case-files'
@@ -431,6 +435,14 @@ Style/OpMethod:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#other-arg'
Enabled: false
+Style/ParallelAssignment:
+ Description: >-
+ Check for simple usages of parallel assignment.
+ It will only warn when the number of variables
+ matches on both sides of the assignment.
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#parallel-assignment'
+ Enabled: false
+
Style/ParenthesesAroundCondition:
Description: >-
Don't use parentheses around the condition of an
@@ -669,6 +681,13 @@ Style/TrailingWhitespace:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-trailing-whitespace'
Enabled: false
+Style/TrailingUnderscoreVariable:
+ Description: >-
+ Checks for the usage of unneeded trailing underscores at the
+ end of parallel variable assignment.
+ AllowNamedUnderscoreVariables: true
+ Enabled: false
+
Style/TrivialAccessors:
Description: 'Prefer attr_* methods to trivial readers/writers.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#attr_family'
@@ -690,11 +709,6 @@ Style/UnneededPercentQ:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-q'
Enabled: false
-Style/UnneededPercentX:
- Description: 'Checks for %x when `` would do.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#percent-x'
- Enabled: false
-
Style/VariableInterpolation:
Description: >-
Don't interpolate global, instance and class variables
@@ -735,23 +749,39 @@ Metrics/AbcSize:
Description: >-
A calculated magnitude based on number of assignments,
branches, and conditions.
- Enabled: false
+ Enabled: true
+ Max: 70
+
+Metrics/CyclomaticComplexity:
+ Description: >-
+ A complexity metric that is strongly correlated to the number
+ of test cases needed to validate a method.
+ Enabled: true
+ Max: 17
+
+Metrics/PerceivedComplexity:
+ Description: >-
+ A complexity metric geared towards measuring complexity for a
+ human reader.
+ Enabled: true
+ Max: 17
+
+Metrics/ParameterLists:
+ Description: 'Avoid parameter lists longer than three or four parameters.'
+ StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
+ Enabled: true
+ Max: 8
Metrics/BlockNesting:
Description: 'Avoid excessive block nesting'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#three-is-the-number-thou-shalt-count'
- Enabled: false
+ Enabled: true
+ Max: 4
Metrics/ClassLength:
Description: 'Avoid classes longer than 100 lines of code.'
Enabled: false
-Metrics/CyclomaticComplexity:
- Description: >-
- A complexity metric that is strongly correlated to the number
- of test cases needed to validate a method.
- Enabled: false
-
Metrics/LineLength:
Description: 'Limit lines to 80 characters.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#80-character-limits'
@@ -762,15 +792,8 @@ Metrics/MethodLength:
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#short-methods'
Enabled: false
-Metrics/ParameterLists:
- Description: 'Avoid parameter lists longer than three or four parameters.'
- StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#too-many-params'
- Enabled: false
-
-Metrics/PerceivedComplexity:
- Description: >-
- A complexity metric geared towards measuring complexity for a
- human reader.
+Metrics/ModuleLength:
+ Description: 'Avoid modules longer than 100 lines of code.'
Enabled: false
#################### Lint ################################
@@ -888,7 +911,7 @@ Lint/RequireParentheses:
Lint/RescueException:
Description: 'Avoid rescuing the Exception class.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-blind-rescues'
- Enabled: false
+ Enabled: true
Lint/ShadowingOuterLocalVariable:
Description: >-
@@ -956,6 +979,12 @@ Rails/ActionFilter:
Description: 'Enforces consistent use of action filter methods.'
Enabled: true
+Rails/Date:
+ Description: >-
+ Checks the correct usage of date aware methods,
+ such as Date.today, Date.current etc.
+ Enabled: false
+
Rails/DefaultScope:
Description: 'Checks if the argument passed to default_scope is a block.'
Enabled: false
@@ -982,6 +1011,12 @@ Rails/ScopeArgs:
Description: 'Checks the arguments of ActiveRecord scopes.'
Enabled: false
+Rails/TimeZone:
+ Description: 'Checks the correct usage of time zone aware methods.'
+ StyleGuide: 'https://github.com/bbatsov/rails-style-guide#time'
+ Reference: 'http://danilenko.org/2012/7/6/rails_timezones'
+ Enabled: false
+
Rails/Validation:
Description: 'Use validates :attribute, hash of validations.'
Enabled: false
diff --git a/.ruby-version b/.ruby-version
index 399088bf465..04b10b4f150 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.6
+2.1.7
diff --git a/CHANGELOG b/CHANGELOG
index 3733a946f41..cd745d3746a 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,23 +1,233 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.2.0 (unreleased)
- - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
+v 8.4.0 (unreleased)
+ - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
+ - Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
+ - Implement new UI for group page
+ - Implement search inside emoji picker
+ - Add API support for looking up a user by username (Stan Hu)
+ - Add project permissions to all project API endpoints (Stan Hu)
+ - Only allow group/project members to mention `@all`
+ - Expose Git's version in the admin area (Trey Davis)
+ - Add "Frequently used" category to emoji picker
+ - Add CAS support (tduehr)
+ - Add link to merge request on build detail page
+ - Revert back upvote and downvote button to the issue and MR pages
+ - Swap position of Assignee and Author selector on Issuables (Zeger-Jan van de Weg)
+ - Add system hook messages for project rename and transfer (Steve Norman)
+ - Fix version check image in Safari
+ - Show 'All' tab by default in the builds page
+ - Fix API project lookups when querying with a namespace with dots (Stan Hu)
+ - Update version check images to use SVG
+ - Validate README format before displaying
+
+v 8.3.3 (unreleased)
+ - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
+ - Enable "Add key" button when user fills in a proper key (Stan Hu)
+
+v 8.3.2
+ - Change single user API endpoint to return more detailed data (Michael Potthoff)
+
+v 8.3.2 (unreleased)
+ - Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
+ - Add support for Google reCAPTCHA in user registration
+
+v 8.3.1
+ - Fix Error 500 when global milestones have slashes (Stan Hu)
+ - Fix Error 500 when doing a search in dashboard before visiting any project (Stan Hu)
+ - Fix LDAP identity and user retrieval when special characters are used
+ - Move Sidekiq-cron configuration to gitlab.yml
+ - Enable forcing Two-Factor authentication sitewide, with optional grace period
+ - Import GitHub Pull Requests into GitLab
+
+v 8.3.0
+ - Bump rack-attack to 4.3.1 for security fix (Stan Hu)
+ - API support for starred projects for authorized user (Zeger-Jan van de Weg)
+ - Add open_issues_count to project API (Stan Hu)
+ - Expand character set of usernames created by Omniauth (Corey Hinshaw)
+ - Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
+ - Provide better diagnostic message upon project creation errors (Stan Hu)
+ - Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
+ - Remove api credentials from link to build_page
+ - Deprecate GitLabCiService making it to always be inactive
+ - Bump gollum-lib to 4.1.0 (Stan Hu)
+ - Fix broken group avatar upload under "New group" (Stan Hu)
+ - Update project repositorize size and commit count during import:repos task (Stan Hu)
+ - Fix API setting of 'public' attribute to false will make a project private (Stan Hu)
+ - Handle and report SSL errors in Web hook test (Stan Hu)
+ - Bump Redis requirement to 2.8 for Sidekiq 4 (Stan Hu)
+ - Fix: Assignee selector is empty when 'Unassigned' is selected (Jose Corcuera)
+ - Add rake tasks for git repository maintainance (Zeger-Jan van de Weg)
+ - Fix 500 error when update group member permission
+ - Trim leading and trailing whitespace of milestone and issueable titles (Jose Corcuera)
+ - Recognize issue/MR/snippet/commit links as references
+ - Backport JIRA features from EE to CE
+ - Add ignore whitespace change option to commit view
+ - Fire update hook from GitLab
+ - Allow account unlock via email
+ - Style warning about mentioning many people in a comment
+ - Fix: sort milestones by due date once again (Greg Smethells)
+ - Migrate all CI::Services and CI::WebHooks to Services and WebHooks
+ - Don't show project fork event as "imported"
+ - Add API endpoint to fetch merge request commits list
+ - Don't create CI status for refs that doesn't have .gitlab-ci.yml, even if the builds are enabled
+ - Expose events API with comment information and author info
+ - Fix: Ensure "Remove Source Branch" button is not shown when branch is being deleted. #3583
+ - Run custom Git hooks when branch is created or deleted.
+ - Fix bug when simultaneously accepting multiple MRs results in MRs that are of "merged" status, but not merged to the target branch
+ - Add languages page to graphs
+ - Block LDAP user when they are no longer found in the LDAP server
+ - Improve wording on project visibility levels (Zeger-Jan van de Weg)
+ - Fix editing notes on a merge request diff
+ - Automatically select default clone protocol based on user preferences (Eirik Lygre)
+ - Make Network page as sub tab of Commits
+ - Add copy-to-clipboard button for Snippets
+ - Add indication to merge request list item that MR cannot be merged automatically
+ - Default target branch to patch-n when editing file in protected branch
+ - Add Builds tab to merge request detail page
+ - Allow milestones, issues and MRs to be created from dashboard and group indexes
+ - Use new style for wiki
+ - Use new style for milestone detail page
+ - Fix sidebar tooltips when collapsed
+ - Prevent possible XSS attack with award-emoji
+ - Upgraded Sidekiq to 4.x
+ - Accept COPYING,COPYING.lesser, and licence as license file (Zeger-Jan van de Weg)
+ - Fix emoji aliases problem
+ - Fix award-emojis Flash alert's width
+ - Fix deleting notes on a merge request diff
+ - Display referenced merge request statuses in the issue description (Greg Smethells)
+ - Implement new sidebar for issue and merge request pages
+ - Emoji picker improvements
+ - Suppress warning about missing `.gitlab-ci.yml` if builds are disabled
+ - Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
+ - Persist runners registration token in database
+ - Fix online editor should not remove newlines at the end of the file
+ - Expose Git's version in the admin area
+ - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
+
+v 8.2.3
+ - Fix application settings cache not expiring after changes (Stan Hu)
+ - Fix Error 500s when creating global milestones with Unicode characters (Stan Hu)
+ - Update documentation for "Guest" permissions
+ - Properly convert Emoji-only comments into Award Emojis
+ - Enable devise paranoid mode to prevent user enumeration attack
+ - Webhook payload has an added, modified and removed properties for each commit
+ - Fix 500 error when creating a merge request that removes a submodule
+
+v 8.2.2
+ - Fix 404 in redirection after removing a project (Stan Hu)
+ - Ensure cached application settings are refreshed at startup (Stan Hu)
+ - Fix Error 500 when viewing user's personal projects from admin page (Stan Hu)
+ - Fix: Raw private snippets access workflow
+ - Prevent "413 Request entity too large" errors when pushing large files with LFS
+ - Fix: As an admin, cannot add oneself as a member to a group/project
+ - Fix invalid links within projects dashboard header
+ - Make current user the first user in assignee dropdown in issues detail page (Stan Hu)
+ - Fix: duplicate email notifications on issue comments
+
+v 8.2.1
+ - Forcefully update builds that didn't want to update with state machine
+ - Fix: saving GitLabCiService as Admin Template
+
+v 8.2.0
+ - Improved performance of finding projects and groups in various places
+ - Improved performance of rendering user profile pages and Atom feeds
+ - Expose build artifacts path as config option
+ - Fix grouping of contributors by email in graph.
+ - Improved performance of finding issues with/without labels
+ - Fix Drone CI service template not saving properly (Stan Hu)
+ - Fix avatars not showing in Atom feeds and project issues when Gravatar disabled (Stan Hu)
+ - Added a GitLab specific profiling tool called "Sherlock" (see GitLab CE merge request #1749)
+ - Upgrade gitlab_git to 7.2.20 and rugged to 0.23.3 (Stan Hu)
+ - Improved performance of finding users by one of their Email addresses
+ - Add allow_failure field to commit status API (Stan Hu)
+ - Commits without .gitlab-ci.yml are marked as skipped
+ - Save detailed error when YAML syntax is invalid
+ - Since GitLab CI is enabled by default, remove enabling it by pushing .gitlab-ci.yml
+ - Added build artifacts
- Improved performance of replacing references in comments
- - Fix duplicate repositories in GitHub import page (Stan Hu)
- - Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
- Show last project commit to default branch on project home page
- Highlight comment based on anchor in URL
- Adds ability to remove the forked relationship from project settings screen. (Han Loong Liauw)
- Improved performance of sorting milestone issues
- Allow users to select the Files view as default project view (Cristian Bica)
-
-v 8.1.0 (unreleased)
+ - Show "Empty Repository Page" for repository without branches (Artem V. Navrotskiy)
+ - Fix: Inability to reply to code comments in the MR view, if the MR comes from a fork
+ - Use git follow flag for commits page when retrieve history for file or directory
+ - Show merge request CI status on merge requests index page
+ - Send build name and stage in CI notification e-mail
+ - Extend yml syntax for only and except to support specifying repository path
+ - Enable shared runners to all new projects
+ - Bump GitLab-Workhorse to 0.4.1
+ - Allow to define cache in `.gitlab-ci.yml`
+ - Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
+ - Remove deprecated CI events from project settings page
+ - Use issue editor as cross reference comment author when issue is edited with a new mention.
+ - Add graphs of commits ahead and behind default branch (Jeff Stubler)
+ - Improve personal snippet access workflow (Douglas Alexandre)
+ - [API] Add ability to fetch the commit ID of the last commit that actually touched a file
+ - Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
+ - Add "New file" link to dropdown on project page
+ - Include commit logs in project search
+ - Add "added", "modified" and "removed" properties to commit object in webhook
+ - Rename "Back to" links to "Go to" because its not always a case it point to place user come from
+ - Allow groups to appear in the search results if the group owner allows it
+ - Add email notification to former assignee upon unassignment (Adam Lieskovský)
+ - New design for project graphs page
+ - Remove deprecated dumped yaml file generated from previous job definitions
+ - Show specific runners from projects where user is master or owner
+ - MR target branch is now visible on a list view when it is different from project's default one
+ - Improve Continuous Integration graphs page
+ - Make color of "Accept Merge Request" button consistent with current build status
+ - Add ignore white space option in merge request diff and commit and compare view
+ - Ability to add release notes (markdown text and attachments) to git tags (aka Releases)
+ - Relative links from a repositories README.md now link to the default branch
+ - Fix trailing whitespace issue in merge request/issue title
+ - Fix bug when milestone/label filter was empty for dashboard issues page
+ - Add ability to create milestone in group projects from single form
+ - Add option to create merge request when editing/creating a file (Dirceu Tiegs)
+ - Prevent the last owner of a group from being able to delete themselves by 'adding' themselves as a master (James Lopez)
+ - Add Award Emoji to issue and merge request pages
+
+v 8.1.4
+ - Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
+ - Prevent redirect loop when home_page_url is set to the root URL
+ - Fix incoming email config defaults
+ - Remove CSS property preventing hard tabs from rendering in Chromium 45 (Stan Hu)
+
+v 8.1.3
+ - Force update refs/merge-requests/X/head upon a push to the source branch of a merge request (Stan Hu)
+ - Spread out runner contacted_at updates
+ - Use issue editor as cross reference comment author when issue is edited with a new mention
+ - Add Facebook authentication
+
+v 8.1.2
+ - Fix cloning Wiki repositories via HTTP (Stan Hu)
+ - Add migration to remove satellites directory
+ - Fix specific runners visibility
+ - Fix 500 when editing CI service
+ - Require CI jobs to be named
+ - Fix CSS for runner status
+ - Fix CI badge
+ - Allow developer to manage builds
+
+v 8.1.1
+ - Removed, see 8.1.2
+
+v 8.1.0
+ - Ensure MySQL CI limits DB migrations occur after the fields have been created (Stan Hu)
+ - Fix duplicate repositories in GitHub import page (Stan Hu)
+ - Redirect to a default path if HTTP_REFERER is not set (Stan Hu)
+ - Adds ability to create directories using the web editor (Ben Ford)
+ - Cleanup stuck CI builds
- Send an email to admin email when a user is reported for spam (Jonathan Rochkind)
- Show notifications button when user is member of group rather than project (Grzegorz Bizon)
- Fix bug preventing mentioned issued from being closed when MR is merged using fast-forward merge.
- Fix nonatomic database update potentially causing project star counts to go negative (Stan Hu)
+ - Don't show "Add README" link in an empty repository if user doesn't have access to push (Stan Hu)
- Fix error preventing displaying of commit data for a directory with a leading dot (Stan Hu)
- Speed up load times of issue detail pages by roughly 1.5x
+ - Fix CI rendering regressions
- If a merge request is to close an issue, show this on the issue page (Zeger-Jan van de Weg)
- Add a system note and update relevant merge requests when a branch is deleted or re-added (Stan Hu)
- Make diff file view easier to use on mobile screens (Stan Hu)
@@ -30,6 +240,7 @@ v 8.1.0 (unreleased)
- Remove CI migration task
- Improved performance of finding projects by their namespace
- Fix bug where transferring a project would result in stale commit links (Stan Hu)
+ - Fix build trace updating
- Include full path of source and target branch names in New Merge Request page (Stan Hu)
- Add user preference to view activities as default dashboard (Stan Hu)
- Add option to admin area to sign in as a specific user (Pavel Forkert)
@@ -72,6 +283,7 @@ v 8.1.0 (unreleased)
- Fix position of hamburger in header for smaller screens (Han Loong Liauw)
- Fix bug where Emojis in Markdown would truncate remaining text (Sakata Sinji)
- Persist filters when sorting on admin user page (Jerry Lukins)
+ - Update style of snippets pages (Han Loong Liauw)
- Allow dashboard and group issues/MRs to be filtered by label
- Add spellcheck=false to certain input fields
- Invalidate stored service password if the endpoint URL is changed
@@ -84,11 +296,11 @@ v 8.1.0 (unreleased)
- Let gitlab-git-http-server generate and serve 'git archive' downloads
- Optimize query when filtering on issuables (Zeger-Jan van de Weg)
- Fix padding of outdated discussion item.
+ - Animate the logo on hover
v 8.0.5
- Correct lookup-by-email for LDAP logins
- Fix loading spinner sometimes not being hidden on Merge Request tab switches
- - Animate the logo on hover
v 8.0.4
- Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
@@ -120,7 +332,6 @@ v 8.0.2
- Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie)
v 8.0.1
- - Remove git refs used internally by GitLab from network graph (Stan Hu)
- Improve CI migration procedure and documentation
v 8.0.0
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 69abadb151a..b9c2b3d2f8e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,104 +1,267 @@
# Contribute to GitLab
-Thank you for your interest in contributing to GitLab.
-This guide details how contribute to GitLab in a way that is efficient for everyone.
-If you have read this guide and want to know how the GitLab core-team operates please see [the GitLab contributing process](PROCESS.md).
+Thank you for your interest in contributing to GitLab. This guide details how
+to contribute to GitLab in a way that is efficient for everyone.
+
+GitLab comes into two flavors, GitLab Community Edition (CE) our free and open
+source edition, and GitLab Enterprise Edition (EE) which is our commercial
+edition. Throughout this guide you will see references to CE and EE for
+abbreviation.
+
+If you have read this guide and want to know how the GitLab [core-team][]
+operates please see [the GitLab contributing process](PROCESS.md).
## Contributor license agreement
-By submitting code as an individual you agree to the [individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md). By submitting code as an entity you agree to the [corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
+By submitting code as an individual you agree to the
+[individual contributor license agreement](doc/legal/individual_contributor_license_agreement.md).
+By submitting code as an entity you agree to the
+[corporate contributor license agreement](doc/legal/corporate_contributor_license_agreement.md).
## Security vulnerability disclosure
-Please report suspected security vulnerabilities in private to support@gitlab.com, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
+Please report suspected security vulnerabilities in private to
+`support@gitlab.com`, also see the
+[disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/).
+Please do **NOT** create publicly viewable issues for suspected security
+vulnerabilities.
## Closing policy for issues and merge requests
-GitLab is a popular open source project and the capacity to deal with issues and merge requests is limited. Out of respect for our volunteers, issues and merge requests not in line with the guidelines listed in this document may be closed without notice.
+GitLab is a popular open source project and the capacity to deal with issues
+and merge requests is limited. Out of respect for our volunteers, issues and
+merge requests not in line with the guidelines listed in this document may be
+closed without notice.
-Please treat our volunteers with courtesy and respect, it will go a long way towards getting your issue resolved.
+Please treat our volunteers with courtesy and respect, it will go a long way
+towards getting your issue resolved.
-Issues and merge requests should be in English and contain appropriate language for audiences of all ages.
+Issues and merge requests should be in English and contain appropriate language
+for audiences of all ages.
## Helping others
-Please help other GitLab users when you can.
-The channnels people will reach out on can be found on the [getting help page](https://about.gitlab.com/getting-help/).
-Sign up for the mailinglist, answer GitLab questions on StackOverflow or respond in the irc channel.
-You can also sign up on [CodeTriage](http://www.codetriage.com/gitlabhq/gitlabhq) to help with one issue every day.
+Please help other GitLab users when you can. The channels people will reach out
+on can be found on the [getting help page][].
+
+Sign up for the mailing list, answer GitLab questions on StackOverflow or
+respond in the IRC channel. You can also sign up on [CodeTriage][] to help with
+the remaining issues on the GitHub issue tracker.
+
+## I want to contribute!
+
+If you want to contribute to GitLab, but are not sure where to start,
+look for [issues with the label `up-for-grabs`][up-for-grabs]. These issues
+will be of reasonable size and challenge, for anyone to start contributing to
+GitLab.
+
+This was inspired by [an article by Kent C. Dodds][medium-up-for-grabs].
## Issue tracker
-To get support for your particular problem please use the [getting help channels](https://about.gitlab.com/getting-help/).
+To get support for your particular problem please use the
+[getting help channels](https://about.gitlab.com/getting-help/).
+
+The [GitLab CE issue tracker on GitLab.com][ce-tracker] is for bugs concerning
+the latest GitLab release and [feature proposals](#feature-proposals).
+
+When submitting an issue please conform to the issue submission guidelines
+listed below. Not all issues will be addressed and your issue is more likely to
+be addressed if you submit a merge request which partially or fully solves
+the issue.
+
+If you're unsure where to post, post to the [mailing list][google-group] or
+[Stack Overflow][stackoverflow] first. There are a lot of helpful GitLab users
+there who may be able to help you quickly. If your particular issue turns out
+to be a bug, it will find its way from there.
+
+If it happens that you know the solution to an existing bug, please first
+open the issue in order to keep track of it and then open the relevant merge
+request that potentially fixes it.
+
+### Feature proposals
+
+To create a feature proposal for CE and CI, open an issue on the
+[issue tracker of CE][ce-tracker].
-The [GitLab CE issue tracker on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues) is only for obvious errors in the latest [stable or development release of GitLab](MAINTENANCE.md). If something is wrong but it is not a regression compared to older versions of GitLab please do not open an issue but a feature request. When submitting an issue please conform to the issue submission guidelines listed below. Not all issues will be addressed and your issue is more likely to be addressed if you submit a merge request which partially or fully addresses the issue.
+For feature proposals for EE, open an issue on the
+[issue tracker of EE][ee-tracker].
-Do not use the issue tracker for feature requests. We have a specific [feature request forum](http://feedback.gitlab.com) for this purpose. Please keep feature requests as small and simple as possible, complex ones might be edited to make them small and simple.
+In order to help track the feature proposals, we have created a
+[`feature proposal`][fpl] label. For the time being, users that are not members
+of the project cannot add labels. You can instead ask one of the [core team][]
+members to add the label `feature proposal` to the issue.
-Please send a merge request with a tested solution or a merge request with a failing test instead of opening an issue if you can. If you're unsure where to post, post to the [mailing list](https://groups.google.com/forum/#!forum/gitlabhq) or [Stack Overflow](http://stackoverflow.com/questions/tagged/gitlab) first. There are a lot of helpful GitLab users there who may be able to help you quickly. If your particular issue turns out to be a bug, it will find its way from there.
+Please keep feature proposals as small and simple as possible, complex ones
+might be edited to make them small and simple.
+
+For changes in the interface, it can be helpful to create a mockup first.
+If you want to create something yourself, consider opening an issue first to
+discuss whether it is interesting to include this in GitLab.
### Issue tracker guidelines
-**[Search the issues](https://gitlab.com/gitlab-org/gitlab-ce/issues)** for similar entries before submitting your own, there's a good chance somebody else had the same issue. Show your support with `:+1:` and/or join the discussion. Please submit issues in the following format (as the first post):
+**[Search the issue tracker][ce-tracker]** for similar entries before
+submitting your own, there's a good chance somebody else had the same issue or
+feature proposal. Show your support with an award emoji and/or join the
+discussion.
+
+Please submit bugs using the following template in the issue description area.
+The text in the parenthesis is there to help you with what to include. Omit it
+when submitting the actual issue. You can copy-paste it and then edit as you
+see fit.
+
+```
+## Summary
+
+(Summarize your issue in one sentence - what goes wrong, what did you expect to happen)
+
+## Steps to reproduce
+
+(How one can reproduce the issue - this is very important)
+
+## Expected behavior
+
+(What you should see instead)
+
+## Relevant logs and/or screenshots
+
+(Paste any relevant logs - please use code blocks (```) to format console output,
+logs, and code as it's very hard to read otherwise.)
+
+## Output of checks
-1. **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen)
-1. **Steps to reproduce:** How can we reproduce the issue
-1. **Expected behavior:** Describe your issue in detail
-1. **Observed behavior**
-1. **Relevant logs and/or screenshots:** Please use code blocks (\`\`\`) to format console output, logs, and code as it's very hard to read otherwise.
-1. **Output of checks**
- * Results of GitLab [Application Check](doc/install/installation.md#check-application-status) (`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`); we will only investigate if the tests are passing
- * Version of GitLab you are running; we will only investigate issues in the latest stable and development releases as per the [maintenance policy](MAINTENANCE.md)
- * Add the last commit SHA-1 of the GitLab version you used to replicate the issue (obtainable from the help page)
- * Describe your setup (use relevant parts from `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
-1. **Possible fixes**: If you can, link to the line of code that might be responsible for the problem
+### Results of GitLab Application Check
+
+(For installations with omnibus-gitlab package run and paste the output of:
+sudo gitlab-rake gitlab:check SANITIZE=true)
+
+(For installations from source run and paste the output of:
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
+
+(we will only investigate if the tests are passing)
+
+### Results of GitLab Environment Info
+
+(For installations with omnibus-gitlab package run and paste the output of:
+sudo gitlab-rake gitlab:env:info)
+
+(For installations from source run and paste the output of:
+sudo -u git -H bundle exec rake gitlab:env:info)
+
+## Possible fixes
+
+(If you can, link to the line of code that might be responsible for the problem)
+
+```
+
+### Issue weight
+
+Issue weight allows us to get an idea of the amount of work required to solve
+one or multiple issues. This makes it possible to schedule work more accurately.
+
+You are encouraged to set the weight of any issue. Following the guidelines
+below will make it easy to manage this, without unnecessary overhead.
+
+1. Set weight for any issue at the earliest possible convenience
+1. If you don't agree with a set weight, discuss with other developers until
+consensus is reached about the weight
+1. Issue weights are an abstract measurement of complexity of the issue. Do not
+relate issue weight directly to time. This is called [anchoring](https://en.wikipedia.org/wiki/Anchoring)
+and something you want to avoid.
+1. Something that has a weight of 1 (or no weight) is really small and simple.
+Something that is 9 is rewriting a large fundamental part of GitLab,
+which might lead to many hard problems to solve. Changing some text in GitLab
+is probably 1, adding a new Git Hook maybe 4 or 5, big features 7-9.
+1. If something is very large, it should probably be split up in multiple
+issues or chunks. You can simply not set the weight of a parent issue and set
+weights to children issues.
## Merge requests
-We welcome merge requests with fixes and improvements to GitLab code, tests, and/or documentation. The features we would really like a merge request for are listed with the [status 'accepting merge requests' on our feature request forum](http://feedback.gitlab.com/forums/176466-general/status/796455) but other improvements are also welcome. If you want to add a new feature that is not marked it is best to first create a feedback issue (if there isn't one already) and leave a comment asking for it to be marked accepting merge requests. Please include screenshots or wireframes if the feature will also change the UI.
+We welcome merge requests with fixes and improvements to GitLab code, tests,
+and/or documentation. The features we would really like a merge request for are
+listed with the label [`Accepting Merge Requests` on our issue tracker for CE][accepting-mrs-ce]
+and [EE][accepting-mrs-ee] but other improvements are also welcome.
+
+If you want to add a new feature that is not labeled it is best to first create
+a feedback issue (if there isn't one already) and leave a comment asking for it
+to be marked as `Accepting merge requests`. Please include screenshots or
+wireframes if the feature will also change the UI.
-Merge requests can be filed either at [gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests) or [github.com](https://github.com/gitlabhq/gitlabhq/pulls).
+Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at
+[github.com][github-mr-tracker].
-If you are new to GitLab development (or web development in general), search for the label `easyfix` ([gitlab.com](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=easyfix), [github](https://github.com/gitlabhq/gitlabhq/labels/easyfix)). Those are issues easy to fix, marked by the GitLab core-team. If you are unsure how to proceed but want to help, mention one of the core-team members to give you a hint.
+If you are new to GitLab development (or web development in general), see the
+[I want to contribute!](#i-want-to-contribute) section to get you started with
+some potentially easy issues.
-To start with GitLab download the [GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit) and see [Development section](doc/development/README.md) in the help file.
+To start with GitLab development download the [GitLab Development Kit][gdk] and
+see the [Development section](doc/development/README.md) for some guidelines.
### Merge request guidelines
-If you can, please submit a merge request with the fix or improvements including tests. If you don't know how to fix the issue but can write a test that exposes the issue we will accept that as well. In general bug fixes that include a regression test are merged quickly while new features without proper tests are least likely to receive timely feedback. The workflow to make a merge request is as follows:
+If you can, please submit a merge request with the fix or improvements
+including tests. If you don't know how to fix the issue but can write a test
+that exposes the issue we will accept that as well. In general bug fixes that
+include a regression test are merged quickly while new features without proper
+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. 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
-1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
-1. Push the commit to your fork
+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
+1. If you have multiple commits please combine them into one commit by
+ [squashing them][git-squash]
+1. Push the commit(s) to your fork
1. Submit a merge request (MR) to the master branch
1. The MR title should describe the change you want to make
-1. The MR description should give a motive for your change and the method you used to achieve it
+1. The MR description should give a motive for your change and the method you
+ used to achieve it
1. If the MR changes the UI it should include before and after screenshots
-1. If the MR changes CSS classes please include the list of affected pages `grep css-class ./app -R`
-1. Link relevant [issues](https://gitlab.com/gitlab-org/gitlab-ce/issues) and/or [feature requests](http://feedback.gitlab.com/) from the merge request description and leave a comment on them with a link back to the MR
-1. Be prepared to answer questions and incorporate feedback even if requests for this arrive weeks or months after your MR submission
-1. If your MR touches code that executes shell commands, make sure it adheres to the [shell command guidelines]( doc/development/shell_commands.md).
-1. Also have a look at the [shell command guidelines](doc/development/shell_commands.md) if your code reads or opens files, or handles paths to files on disk.
-
-The **official merge window** is in the beginning of the month from the 1st to the 7th day of the month. The best time to submit a MR and get feedback fast.
-Before this time the GitLab B.V. team is still dealing with work that is created by the monthly release such as regressions requiring patch releases.
-After the 7th it is already getting closer to the release date of the next version. This means there is less time to fix the issues created by merging large new features.
-
-Please keep the change in a single MR **as small as possible**. If you want to contribute a large feature think very hard what the minimum viable change is. Can you split functionality? Can you only submit the backend/API code? Can you start with a very simple UI? Can you do part of the refactor? The increased reviewability of small MR's that leads to higher code quality is more important to us than having a minimal commit log. The smaller a MR is the more likely it is it will be merged (quickly), after that you can send more MR's to enhance it.
-
-For examples of feedback on merge requests please look at already [closed merge requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed). If you would like quick feedback on your merge request feel free to mention one of the Merge Marshalls of [the core-team](https://about.gitlab.com/core-team/). Please ensure that your merge request meets the contribution acceptance criteria.
+1. If the MR changes CSS classes please include the list of affected pages
+ `grep css-class ./app -R`
+1. Link any relevant [issues][ce-tracker] in the merge request description and
+ leave a comment on them with a link back to the MR
+1. Be prepared to answer questions and incorporate feedback even if requests
+ for this arrive weeks or months after your MR submission
+1. If your MR touches code that executes shell commands, reads or opens files or
+ handles paths to files on disk, make sure it adheres to the
+ [shell command guidelines](doc/development/shell_commands.md)
+1. If your code creates new files on disk please read the
+ [shared files guidelines](doc/development/shared_files.md).
+
+The **official merge window** is in the beginning of the month from the 1st to
+the 7th day of the month. This is the best time to submit an MR and get
+feedback fast. Before this time the GitLab Inc. team is still dealing with work
+that is created by the monthly release such as regressions requiring patch
+releases. After the 7th it is already getting closer to the release date of the
+next version. This means there is less time to fix the issues created by
+merging large new features.
+
+Please keep the change in a single MR **as small as possible**. If you want to
+contribute a large feature think very hard what the minimum viable change is.
+Can you split the functionality? Can you only submit the backend/API code? Can
+you start with a very simple UI? Can you do part of the refactor? The increased
+reviewability of small MRs that leads to higher code quality is more important
+to us than having a minimal commit log. The smaller an MR is the more likely it
+is it will be merged (quickly). After that you can send more MRs to enhance it.
+
+For examples of feedback on merge requests please look at already
+[closed merge requests][]. If you would like quick feedback on your merge
+request feel free to mention one of the Merge Marshalls of the [core team][].
+Please ensure that your merge request meets the contribution acceptance criteria.
## Definition of done
-If you contribute to GitLab please know that changes involve more than just code.
-We have the following [definition of done](http://guide.agilealliance.org/guide/definition-of-done.html).
-Please ensure you support the feature you contribute through all of these steps.
+If you contribute to GitLab please know that changes involve more than just
+code. We have the following [definition of done][]. Please ensure you support
+the feature you contribute through all of these steps.
-1. Description explaning the relevancy (see following item)
+1. Description explaining the relevancy (see following item)
1. Working and clean code that is commented where needed
1. Unit and integration tests that pass on the CI server
1. Documented in the /doc directory
@@ -110,14 +273,16 @@ Please ensure you support the feature you contribute through all of these steps.
1. Community questions answered
1. Answers to questions radiated (in docs/wiki/etc.)
-If you add a dependency in GitLab (such as an operating system package) please consider updating the following and note the applicability of each in your merge request:
+If you add a dependency in GitLab (such as an operating system package) please
+consider updating the following and note the applicability of each in your
+merge request:
1. Note the addition in the release blog post (create one if it doesn't exist yet) https://gitlab.com/gitlab-com/www-gitlab-com/merge_requests/
1. Upgrade guide, for example https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/7.5-to-7.6.md
1. Upgrader https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/upgrader.md#2-run-gitlab-upgrade-tool
1. Installation guide https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies
1. GitLab Development Kit https://gitlab.com/gitlab-org/gitlab-development-kit
-1. Test suite https://gitlab.com/gitlab-org/gitlab-ci/blob/master/doc/examples/configure_a_runner_to_run_the_gitlab_ce_test_suite.md
+1. Test suite https://gitlab.com/gitlab-org/gitlab-ce/blob/master/scripts/prepare_build.sh
1. Omnibus package creator https://gitlab.com/gitlab-org/omnibus-gitlab
## Merge request description format
@@ -125,59 +290,111 @@ If you add a dependency in GitLab (such as an operating system package) please c
1. What does this MR do?
1. Are there points in the code the reviewer needs to double check?
1. Why was this MR needed?
-1. What are the relevant issue numbers / [Feature requests](http://feedback.gitlab.com/)?
+1. What are the relevant issue numbers?
1. Screenshots (if relevant)
## Contribution acceptance criteria
1. The change is as small as possible (see the above paragraph for details)
-1. Include proper tests and make all tests pass (unless it contains a test exposing a bug in existing code)
-1. All tests have to pass, if you suspect a failing CI build is unrelated to your contribution ask for tests to be restarted. See [the CI setup document](http://doc.gitlab.com/ce/development/ci_setup.html) on who you can ask for test restart.
-1. Initially contains a single commit (please use `git rebase -i` to squash commits)
-1. Can merge without problems (if not please merge `master`, never rebase commits pushed to the remote server)
+1. Include proper tests and make all tests pass (unless it contains a test
+ exposing a bug in existing code)
+1. If you suspect a failing CI build is unrelated to your contribution, you may
+ try and restart the failing CI job or ask a developer to fix the
+ aforementioned failing test
+1. Your MR initially contains a single commit (please use `git rebase -i` to
+ squash commits)
+1. Your changes can merge without problems (if not please merge `master`, never
+ rebase commits pushed to the remote server)
1. Does not break any existing functionality
-1. Fixes one specific issue or implements one specific feature (do not combine things, send separate merge requests if needed)
-1. Migrations should do only one thing (eg: either create a table, move data to a new table or remove an old table) to aid retrying on failure
+1. Fixes one specific issue or implements one specific feature (do not combine
+ things, send separate merge requests if needed)
+1. Migrations should do only one thing (eg: either create a table, move data to
+ a new table or remove an old table) to aid retrying on failure
1. Keeps the GitLab code base clean and well structured
1. Contains functionality we think other users will benefit from too
1. Doesn't add configuration options since they complicate future changes
-1. Changes after submitting the merge request should be in separate commits (no squashing). You will be asked to squash when the review is over, before merging.
-1. It conforms to the following style guides.
- If your change touches a line that does not follow the style,
- modify the 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. Changes after submitting the merge request should be in separate commits
+ (no squashing). If necessary, you will be asked to squash when the review is
+ over, before merging.
+1. It conforms to the following style guides:
+ * If your change touches a line that does not follow the style, modify the
+ 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.
## Style guides
1. [Ruby](https://github.com/bbatsov/ruby-style-guide).
- Important sections include [Source Code Layout](https://github.com/bbatsov/ruby-style-guide#source-code-layout)
- and [Naming](https://github.com/bbatsov/ruby-style-guide#naming). Use:
+ Important sections include [Source Code Layout][rss-source] and
+ [Naming][rss-naming]. Use:
- multi-line method chaining style **Option B**: dot `.` on previous line
- string literal quoting style **Option A**: single quoted by default
1. [Rails](https://github.com/bbatsov/rails-style-guide)
-1. [Testing](https://github.com/thoughtbot/guides/tree/master/style#testing)
-1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style#coffeescript)
-1. [Shell commands](doc/development/shell_commands.md) created by GitLab contributors to enhance security
+1. [Testing](https://github.com/thoughtbot/guides/tree/master/style/testing)
+1. [CoffeeScript](https://github.com/thoughtbot/guides/tree/master/style/coffeescript)
+1. [Shell commands](doc/development/shell_commands.md) created by GitLab
+ contributors to enhance security
1. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
1. [Database Migrations](doc/development/migration_style_guide.md)
1. [Documentation styleguide](doc_styleguide.md)
-1. Interface text should be written subjectively instead of objectively. It should be the gitlab core team addressing a person. It should be written in present time and never use past tense (has been/was). For example instead of "prohibited this user from being saved due to the following errors:" the text should be "sorry, we could not create your account because:". Also these [excellent writing guidelines](https://github.com/NARKOZ/guides#writing).
+1. Interface text should be written subjectively instead of objectively. It
+ should be the GitLab core team addressing a person. It should be written in
+ present time and never use past tense (has been/was). For example instead
+ of _prohibited this user from being saved due to the following errors:_ the
+ text should be _sorry, we could not create your account because:_
-This is also the style used by linting tools such as [RuboCop](https://github.com/bbatsov/rubocop), [PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
+This is also the style used by linting tools such as
+[RuboCop](https://github.com/bbatsov/rubocop),
+[PullReview](https://www.pullreview.com/) and [Hound CI](https://houndci.com).
## Code of conduct
-As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
-
-We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
-
-Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
-
-Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
-
-This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
-
-Instances of abusive, harassing, or otherwise unacceptable behavior can be reported by emailing contact@gitlab.com
-
-This Code of Conduct is adapted from the [Contributor Covenant](http:contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/)
+As contributors and maintainers of this project, we pledge to respect all
+people who contribute through reporting issues, posting feature requests,
+updating documentation, submitting pull requests or patches, and other
+activities.
+
+We are committed to making participation in this project a harassment-free
+experience for everyone, regardless of level of experience, gender, gender
+identity and expression, sexual orientation, disability, personal appearance,
+body size, race, ethnicity, age, or religion.
+
+Examples of unacceptable behavior by participants include the use of sexual
+language or imagery, derogatory comments or personal attacks, trolling, public
+or private harassment, insults, or other unprofessional conduct.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct. Project maintainers who do not
+follow the Code of Conduct may be removed from the project team.
+
+This code of conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community.
+
+Instances of abusive, harassing, or otherwise unacceptable behavior can be
+reported by emailing `contact@gitlab.com`.
+
+This Code of Conduct is adapted from the [Contributor Covenant][], version 1.1.0,
+available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/).
+
+[core team]: https://about.gitlab.com/core-team/
+[getting help page]: https://about.gitlab.com/getting-help/
+[Codetriage]: http://www.codetriage.com/gitlabhq/gitlabhq
+[up-for-grabs]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=up-for-grabs
+[medium-up-for-grabs]: https://medium.com/@kentcdodds/first-timers-only-78281ea47455
+[ce-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/issues
+[ee-tracker]: https://gitlab.com/gitlab-org/gitlab-ee/issues
+[google-group]: https://groups.google.com/forum/#!forum/gitlabhq
+[stackoverflow]: https://stackoverflow.com/questions/tagged/gitlab
+[fpl]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=feature+proposal
+[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
+[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
+[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
+[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
+[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
+[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
+[closed merge requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
+[definition of done]: http://guide.agilealliance.org/guide/definition-of-done.html
+[Contributor Covenant]: http://contributor-covenant.org
+[rss-source]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#source-code-layout
+[rss-naming]: https://github.com/bbatsov/ruby-style-guide/blob/master/README.md#naming
diff --git a/GITLAB_GIT_HTTP_SERVER_VERSION b/GITLAB_GIT_HTTP_SERVER_VERSION
deleted file mode 100644
index 0d91a54c7d4..00000000000
--- a/GITLAB_GIT_HTTP_SERVER_VERSION
+++ /dev/null
@@ -1 +0,0 @@
-0.3.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 57cf282ebbc..d48d3702aed 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.5
+2.6.9
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
new file mode 100644
index 00000000000..4b9fcbec101
--- /dev/null
+++ b/GITLAB_WORKHORSE_VERSION
@@ -0,0 +1 @@
+0.5.1
diff --git a/Gemfile b/Gemfile
index 9254ce2ccfa..3ce4ba4a2a5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,10 @@
source "https://rubygems.org"
-gem 'rails', '4.1.12'
+gem 'rails', '4.2.4'
+gem 'rails-deprecated_sanitizer', '~> 1.0.3'
+
+# Responders respond_to and respond_with
+gem 'responders', '~> 2.0'
# Specify a sprockets version due to security issue
# See https://groups.google.com/forum/#!topic/rubyonrails-security/doAVp0YaTqY
@@ -14,11 +18,13 @@ gem "mysql2", '~> 0.3.16', group: :mysql
gem "pg", '~> 0.18.2', group: :postgres
# Authentication libraries
-gem 'devise', '~> 3.5.2'
+gem 'devise', '~> 3.5.3'
gem 'devise-async', '~> 0.9.0'
-gem 'doorkeeper', '~> 2.1.3'
+gem 'doorkeeper', '~> 2.2.0'
gem 'omniauth', '~> 1.2.2'
gem 'omniauth-bitbucket', '~> 0.0.2'
+gem 'omniauth-cas3', '~> 1.1.2'
+gem 'omniauth-facebook', '~> 3.0.0'
gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.0'
gem 'omniauth-google-oauth2', '~> 0.2.0'
@@ -27,7 +33,10 @@ gem 'omniauth-saml', '~> 1.4.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
gem 'omniauth_crowd'
-gem 'rack-oauth2', '~> 1.0.5'
+gem 'rack-oauth2', '~> 1.2.1'
+
+# reCAPTCHA protection
+gem 'recaptcha', require: 'recaptcha/rails'
# Two-factor authentication
gem 'devise-two-factor', '~> 2.0.0'
@@ -39,7 +48,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.2.19'
+gem "gitlab_git", '~> 7.2.20'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -47,26 +56,19 @@ gem "gitlab_git", '~> 7.2.19'
gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: "omniauth-ldap"
# Git Wiki
-gem 'gollum-lib', '~> 4.0.2'
+gem 'gollum-lib', '~> 4.1.0'
# Language detection
-# GitLab fork of linguist does not require pygments/python dependency.
-# New version of original gem also dropped pygments support but it has strict
-# dependency to unstable rugged version. We have internal issue for replacing
-# fork with original gem when we meet on same rugged version - https://dev.gitlab.org/gitlab/gitlabhq/issues/2052.
-gem "gitlab-linguist", "~> 3.0.1", require: "linguist"
+gem "github-linguist", "~> 4.7.0", require: "linguist"
# API
-gem 'grape', '~> 0.6.1'
+gem 'grape', '~> 0.13.0'
gem 'grape-entity', '~> 0.4.2'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
# Format dates and times
# based on human-friendly examples
-gem "stamp", '~> 0.5.0'
-
-# Enumeration fields
-gem 'enumerize', '~> 0.7.0'
+gem "stamp", '~> 0.6.0'
# Pagination
gem "kaminari", "~> 0.16.3"
@@ -98,9 +100,13 @@ gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.2.9'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~>0.3.6'
+gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2'
+gem 'rouge', '~> 1.10.1'
+
+# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
+gem 'nokogiri', '1.6.7.1'
# Diffs
gem 'diffy', '~> 3.0.3'
@@ -112,7 +118,7 @@ group :unicorn do
end
# State machine
-gem "state_machine", '~> 1.2.0'
+gem "state_machines-activerecord", '~> 0.3.0'
# Run events after state machine commits
gem 'after_commit_queue'
@@ -121,15 +127,15 @@ gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
gem 'sinatra', '~> 1.4.4', require: nil
-gem 'sidekiq', '3.3.0'
-gem 'sidetiq', '~> 0.6.3'
+gem 'sidekiq', '~> 4.0'
+gem 'sidekiq-cron', '~> 0.4.0'
+gem 'redis-namespace'
# HTTP requests
gem "httparty", '~> 0.13.3'
# Colored output to console
-gem "colored", '~> 1.2'
-gem "colorize", '~> 0.5.8'
+gem "colorize", '~> 0.7.0'
# GitLab settings
gem 'settingslogic', '~> 2.0.9'
@@ -157,25 +163,26 @@ gem "gemnasium-gitlab-service", "~> 0.2"
gem "slack-notifier", "~> 1.2.0"
# Asana integration
-gem 'asana', '~> 0.0.6'
+gem 'asana', '~> 0.4.0'
# FogBugz integration
gem 'ruby-fogbugz', '~> 0.2.1'
# d3
-gem 'd3_rails', '~> 3.5.5'
+gem 'd3_rails', '~> 3.5.0'
#cal-heatmap
-gem "cal-heatmap-rails", "~> 0.0.1"
+gem 'cal-heatmap-rails', '~> 3.5.0'
# underscore-rails
-gem "underscore-rails", "~> 1.4.4"
+gem "underscore-rails", "~> 1.8.0"
# Sanitize user input
gem "sanitize", '~> 2.0'
+gem 'babosa', '~> 1.0.2'
# Protect against bruteforcing
-gem "rack-attack", '~> 4.3.0'
+gem "rack-attack", '~> 4.3.1'
# Ace editor
gem 'ace-rails-ap', '~> 2.0.1'
@@ -184,41 +191,49 @@ gem 'ace-rails-ap', '~> 2.0.1'
gem 'mousetrap-rails', '~> 1.4.6'
# Detect and convert string character encoding
-gem 'charlock_holmes', '~> 0.6.9.4'
+gem 'charlock_holmes', '~> 0.7.3'
-gem "sass-rails", '~> 4.0.5'
+gem "sass-rails", '~> 5.0.0'
gem "coffee-rails", '~> 4.1.0'
gem "uglifier", '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
-gem 'jquery-turbolinks', '~> 2.0.1'
+gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
-gem 'bootstrap-sass', '~> 3.0'
+gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2'
-gem 'gitlab_emoji', '~> 0.1'
-gem 'gon', '~> 5.0.0'
-gem 'jquery-atwho-rails', '~> 1.0.0'
-gem 'jquery-rails', '~> 3.1.3'
+gem 'gitlab_emoji', '~> 0.2.0'
+gem 'gon', '~> 6.0.1'
+gem 'jquery-atwho-rails', '~> 1.3.2'
+gem 'jquery-rails', '~> 4.0.0'
gem 'jquery-scrollto-rails', '~> 1.4.3'
-gem 'jquery-ui-rails', '~> 4.2.1'
-gem 'nprogress-rails', '~> 0.1.2.3'
+gem 'jquery-ui-rails', '~> 5.0.0'
+gem 'nprogress-rails', '~> 0.1.6.7'
gem 'raphael-rails', '~> 2.1.2'
gem 'request_store', '~> 1.2.0'
gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
+gem 'net-ssh', '~> 3.0.1'
+
+# Metrics
+group :metrics do
+ gem 'allocations', '~> 1.0', require: false, platform: :mri
+ gem 'method_source', '~> 0.8', require: false
+ gem 'influxdb', '~> 0.2', require: false
+ gem 'connection_pool', '~> 2.0', require: false
+end
group :development do
gem "foreman"
- gem 'brakeman', '3.0.1', require: false
+ gem 'brakeman', '~> 3.1.0', require: false
gem "annotate", "~> 2.6.0"
gem "letter_opener", '~> 1.1.2'
gem 'quiet_assets', '~> 1.0.2'
- gem 'rack-mini-profiler', '~> 0.9.0', require: false
- gem 'rerun', '~> 0.10.0'
+ gem 'rerun', '~> 0.11.0'
gem 'bullet', require: false
- gem 'active_record_query_trace', require: false
- gem 'rack-lineprof', platform: :mri
+ gem 'rblineprof', platform: :mri, require: false
+ gem 'web-console', '~> 2.0'
# Better errors handler
gem 'better_errors', '~> 1.0.1'
@@ -251,7 +266,7 @@ group :development, :test do
gem 'capybara', '~> 2.4.0'
gem 'capybara-screenshot', '~> 1.0.0'
- gem 'poltergeist', '~> 1.6.0'
+ gem 'poltergeist', '~> 1.8.1'
gem 'teaspoon', '~> 1.0.0'
gem 'teaspoon-jasmine', '~> 2.2.0'
@@ -261,9 +276,12 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.0.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.28.0', require: false
+ gem 'rubocop', '~> 0.35.0', require: false
gem 'coveralls', '~> 0.8.2', require: false
gem 'simplecov', '~> 0.10.0', require: false
+ gem 'flog', require: false
+ gem 'flay', require: false
+ gem 'bundler-audit', require: false
gem 'benchmark-ips', require: false
end
@@ -272,7 +290,7 @@ group :test do
gem 'shoulda-matchers', '~> 2.8.0', require: false
gem 'email_spec', '~> 1.6.0'
gem 'webmock', '~> 1.21.0'
- gem 'test_after_commit', '~> 0.2.2'
+ gem 'test_after_commit', '~> 0.4.2'
gem 'sham_rack'
end
diff --git a/Gemfile.lock b/Gemfile.lock
index 53122898b07..ffb7cef0aba 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1,64 +1,72 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (2.3.1)
+ CFPropertyList (2.3.2)
RedCloth (4.2.9)
ace-rails-ap (2.0.1)
- actionmailer (4.1.12)
- actionpack (= 4.1.12)
- actionview (= 4.1.12)
+ actionmailer (4.2.4)
+ actionpack (= 4.2.4)
+ actionview (= 4.2.4)
+ activejob (= 4.2.4)
mail (~> 2.5, >= 2.5.4)
- actionpack (4.1.12)
- actionview (= 4.1.12)
- activesupport (= 4.1.12)
- rack (~> 1.5.2)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ actionpack (4.2.4)
+ actionview (= 4.2.4)
+ activesupport (= 4.2.4)
+ rack (~> 1.6)
rack-test (~> 0.6.2)
- actionview (4.1.12)
- activesupport (= 4.1.12)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ actionview (4.2.4)
+ activesupport (= 4.2.4)
builder (~> 3.1)
erubis (~> 2.7.0)
- active_record_query_trace (1.5)
- activemodel (4.1.12)
- activesupport (= 4.1.12)
+ rails-dom-testing (~> 1.0, >= 1.0.5)
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
+ activejob (4.2.4)
+ activesupport (= 4.2.4)
+ globalid (>= 0.3.0)
+ activemodel (4.2.4)
+ activesupport (= 4.2.4)
builder (~> 3.1)
- activerecord (4.1.12)
- activemodel (= 4.1.12)
- activesupport (= 4.1.12)
- arel (~> 5.0.0)
+ activerecord (4.2.4)
+ activemodel (= 4.2.4)
+ activesupport (= 4.2.4)
+ arel (~> 6.0)
activerecord-deprecated_finders (1.0.4)
- activerecord-session_store (0.1.1)
+ activerecord-session_store (0.1.2)
actionpack (>= 4.0.0, < 5)
activerecord (>= 4.0.0, < 5)
railties (>= 4.0.0, < 5)
- activeresource (4.0.0)
- activemodel (~> 4.0)
- activesupport (~> 4.0)
- rails-observers (~> 0.1.1)
- activesupport (4.1.12)
- i18n (~> 0.6, >= 0.6.9)
+ activesupport (4.2.4)
+ i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
- thread_safe (~> 0.1)
+ thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
acts-as-taggable-on (3.5.0)
activerecord (>= 3.2, < 5)
addressable (2.3.8)
- after_commit_queue (1.1.0)
- rails (>= 3.0)
+ after_commit_queue (1.3.0)
+ activerecord (>= 3.0)
+ allocations (1.0.3)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
- arel (5.0.1.20140414130214)
- asana (0.0.6)
- activeresource (>= 3.2.3)
- asciidoctor (1.5.2)
+ arel (6.0.3)
+ asana (0.4.0)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.9)
+ faraday_middleware-multi_json (~> 0.0)
+ oauth2 (~> 1.0)
+ asciidoctor (1.5.3)
ast (2.1.0)
astrolabe (1.3.1)
parser (~> 2.2)
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
- autoprefixer-rails (5.2.1.2)
+ autoprefixer-rails (6.2.3)
execjs
json
awesome_print (1.2.0)
@@ -66,6 +74,7 @@ GEM
descendants_tracker (~> 0.0.4)
ice_nine (~> 0.11.0)
thread_safe (~> 0.3, >= 0.3.1)
+ babosa (1.0.2)
bcrypt (3.1.10)
benchmark-ips (2.3.0)
better_errors (1.0.1)
@@ -73,26 +82,31 @@ GEM
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (3.3.5)
- autoprefixer-rails (>= 5.0.0.1)
- sass (>= 3.2.19)
- brakeman (3.0.1)
+ bootstrap-sass (3.3.6)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
+ brakeman (3.1.4)
erubis (~> 2.6)
fastercsv (~> 1.5)
haml (>= 3.0, < 5.0)
- highline (~> 1.6.20)
+ highline (>= 1.6.20, < 2.0)
multi_json (~> 1.2)
- ruby2ruby (~> 2.1.1)
- ruby_parser (~> 3.5.0)
+ ruby2ruby (>= 2.1.1, < 2.3.0)
+ ruby_parser (~> 3.7.0)
+ safe_yaml (>= 1.0)
sass (~> 3.0)
+ slim (>= 1.3.6, < 4.0)
terminal-table (~> 1.4)
- browser (1.0.0)
+ browser (1.0.1)
builder (3.2.2)
- bullet (4.14.9)
+ bullet (4.14.10)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.9.0)
- byebug (6.0.2)
- cal-heatmap-rails (0.0.1)
+ bundler-audit (0.4.0)
+ bundler (~> 1.2)
+ thor (~> 0.18)
+ byebug (8.2.1)
+ cal-heatmap-rails (3.5.1)
capybara (2.4.4)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@@ -106,10 +120,9 @@ GEM
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
- celluloid (0.16.0)
- timers (~> 4.0.0)
- charlock_holmes (0.6.9.4)
- chunky_png (1.3.4)
+ cause (0.1)
+ charlock_holmes (0.7.3)
+ chunky_png (1.3.5)
cliver (0.3.2)
coderay (1.1.0)
coercible (1.0.0)
@@ -120,20 +133,21 @@ GEM
coffee-script (2.4.1)
coffee-script-source
execjs
- coffee-script-source (1.9.1.1)
- colored (1.2)
- colorize (0.5.8)
+ coffee-script-source (1.10.0)
+ colorize (0.7.7)
+ concurrent-ruby (1.0.0)
connection_pool (2.2.0)
- coveralls (0.8.2)
+ coveralls (0.8.9)
json (~> 1.8)
rest-client (>= 1.6.8, < 2)
simplecov (~> 0.10.0)
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
- crack (0.4.2)
+ tins (~> 1.6.0)
+ crack (0.4.3)
safe_yaml (~> 1.0.0)
- creole (0.3.8)
- d3_rails (3.5.6)
+ creole (0.5.0)
+ d3_rails (3.5.11)
railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.4.1)
@@ -143,7 +157,7 @@ GEM
activerecord (>= 3.2.0, < 5.0)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devise (3.5.2)
+ devise (3.5.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5)
@@ -152,7 +166,7 @@ GEM
warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
- devise-two-factor (2.0.0)
+ devise-two-factor (2.0.1)
activesupport
attr_encrypted (~> 1.3.2)
devise (~> 3.5.0)
@@ -161,22 +175,20 @@ GEM
diff-lcs (1.2.5)
diffy (3.0.7)
docile (1.1.5)
- domain_name (0.5.24)
+ domain_name (0.5.25)
unf (>= 0.0.5, < 1.0.0)
- doorkeeper (2.1.4)
+ doorkeeper (2.2.2)
railties (>= 3.2)
- dropzonejs-rails (0.7.1)
+ 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)
- enumerize (0.7.0)
- activesupport (>= 3.2)
equalizer (0.0.11)
erubis (2.7.0)
- escape_utils (0.2.4)
+ escape_utils (1.1.0)
eventmachine (1.0.8)
excon (0.45.4)
execjs (2.6.0)
@@ -190,12 +202,21 @@ GEM
multipart-post (>= 1.2, < 3)
faraday_middleware (0.10.0)
faraday (>= 0.7.4, < 0.10)
+ faraday_middleware-multi_json (0.0.6)
+ faraday_middleware
+ multi_json
fastercsv (1.5.5)
ffaker (2.0.0)
ffi (1.9.10)
fission (0.5.0)
CFPropertyList (~> 2.2)
- flowdock (0.7.0)
+ flay (2.6.1)
+ ruby_parser (~> 3.0)
+ sexp_processor (~> 4.0)
+ flog (4.3.2)
+ ruby_parser (~> 3.1, > 3.1.0)
+ sexp_processor (~> 4.4)
+ flowdock (0.7.1)
httparty (~> 0.7)
multi_json
fog (1.25.0)
@@ -213,17 +234,14 @@ GEM
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
opennebula
- fog-brightbox (0.9.0)
+ fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
- fog-core (1.32.1)
+ fog-core (1.35.0)
builder
excon (~> 0.45)
formatador (~> 0.2)
- mime-types
- net-scp (~> 1.1)
- net-ssh (>= 2.1.3)
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
@@ -235,10 +253,10 @@ GEM
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
- fog-sakuracloud (1.0.1)
+ fog-sakuracloud (1.5.0)
fog-core
fog-json
- fog-softlayer (0.4.7)
+ fog-softlayer (1.0.2)
fog-core
fog-json
fog-terremark (0.1.0)
@@ -253,7 +271,7 @@ GEM
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
- font-awesome-rails (4.4.0.0)
+ font-awesome-rails (4.5.0.0)
railties (>= 3.2, < 5.0)
foreman (0.78.0)
thor (~> 0.19.1)
@@ -263,10 +281,15 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
- gemojione (2.0.1)
+ gemojione (2.1.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
+ github-linguist (4.7.3)
+ charlock_holmes (~> 0.7.3)
+ escape_utils (~> 1.1.0)
+ mime-types (>= 1.19)
+ rugged (>= 0.23.0b)
github-markup (1.3.3)
gitlab-flowdock-git-hook (1.0.1)
flowdock (~> 0.7)
@@ -277,39 +300,39 @@ GEM
diff-lcs (~> 1.1)
mime-types (~> 1.15)
posix-spawn (~> 0.3)
- gitlab-linguist (3.0.1)
- charlock_holmes (~> 0.6.6)
- escape_utils (~> 0.2.4)
- mime-types (~> 1.19)
- gitlab_emoji (0.1.1)
- gemojione (~> 2.0)
- gitlab_git (7.2.19)
+ gitlab_emoji (0.2.0)
+ gemojione (~> 2.1)
+ gitlab_git (7.2.22)
activesupport (~> 4.0)
- charlock_holmes (~> 0.6)
- gitlab-linguist (~> 3.0)
- rugged (~> 0.22.2)
+ charlock_holmes (~> 0.7.3)
+ github-linguist (~> 4.7.0)
+ rugged (~> 0.23.3)
gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
pyu-ruby-sasl (~> 0.0.3.1)
rubyntlm (~> 0.3)
+ globalid (0.3.6)
+ activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.0)
gitlab-grit (~> 2.7, >= 2.7.1)
- gollum-lib (4.0.3)
+ gollum-lib (4.1.0)
github-markup (~> 1.3.3)
gollum-grit_adapter (~> 1.0)
nokogiri (~> 1.6.4)
- rouge (~> 1.10.1)
+ rouge (~> 1.9)
sanitize (~> 2.1.0)
stringex (~> 2.5.1)
- gon (5.0.4)
- actionpack (>= 2.3.0)
+ gon (6.0.1)
+ actionpack (>= 3.0)
json
- grape (0.6.1)
+ multi_json
+ request_store (>= 1.0)
+ grape (0.13.0)
activesupport
builder
- hashie (>= 1.2.0)
+ hashie (>= 2.1.0)
multi_json (>= 1.3.2)
multi_xml (>= 0.5.2)
rack (>= 1.3.0)
@@ -327,13 +350,12 @@ GEM
haml (>= 4.0.6, < 5.0)
html2haml (>= 1.0.1)
railties (>= 4.0.1)
- hashie (3.4.2)
- highline (1.6.21)
+ hashie (3.4.3)
+ highline (1.7.8)
hike (1.2.3)
hipchat (1.5.2)
httparty
mimemagic
- hitimes (1.2.3)
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
@@ -345,40 +367,44 @@ GEM
http-cookie (1.0.2)
domain_name (~> 0.5)
http_parser.rb (0.5.3)
- httparty (0.13.5)
+ httparty (0.13.7)
json (~> 1.8)
multi_xml (>= 0.5.2)
- httpclient (2.6.0.1)
+ httpclient (2.7.0.1)
i18n (0.7.0)
- ice_cube (0.11.1)
ice_nine (0.11.1)
inflecto (0.0.2)
+ influxdb (0.2.3)
+ cause
+ json
ipaddress (0.8.0)
- jquery-atwho-rails (1.0.1)
- jquery-rails (3.1.3)
- railties (>= 3.0, < 5.0)
+ jquery-atwho-rails (1.3.2)
+ jquery-rails (4.0.5)
+ rails-dom-testing (~> 1.0)
+ railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jquery-scrollto-rails (1.4.3)
railties (> 3.1, < 5.0)
- jquery-turbolinks (2.0.2)
+ jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
- jquery-ui-rails (4.2.1)
+ jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
json (1.8.3)
- jwt (1.5.1)
+ jwt (1.5.2)
kaminari (0.16.3)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
- kgio (2.9.3)
+ kgio (2.10.0)
launchy (2.4.3)
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
- listen (2.10.1)
- celluloid (~> 0.16.0)
+ listen (3.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
+ loofah (2.0.3)
+ nokogiri (>= 1.5.9)
macaddr (1.7.1)
systemu (~> 2.6.2)
mail (2.6.3)
@@ -387,7 +413,7 @@ GEM
method_source (0.8.2)
mime-types (1.25.1)
mimemagic (0.3.0)
- mini_portile (0.6.2)
+ mini_portile2 (2.0.0)
minitest (5.7.0)
mousetrap-rails (1.4.6)
multi_json (1.11.2)
@@ -395,18 +421,16 @@ GEM
multipart-post (2.0.0)
mysql2 (0.3.20)
nested_form (0.3.2)
- net-ldap (0.11)
- net-scp (1.2.1)
- net-ssh (>= 2.6.5)
- net-ssh (2.9.2)
- netrc (0.10.3)
- newrelic-grape (2.0.0)
+ net-ldap (0.12.1)
+ net-ssh (3.0.1)
+ netrc (0.11.0)
+ newrelic-grape (2.1.0)
grape
newrelic_rpm
newrelic_rpm (3.9.4.245)
- nokogiri (1.6.6.2)
- mini_portile (~> 0.6.0)
- nprogress-rails (0.1.2.3)
+ nokogiri (1.6.7.1)
+ mini_portile2 (~> 2.0.0.rc2)
+ nprogress-rails (0.1.6.7)
oauth (0.4.7)
oauth2 (1.0.0)
faraday (>= 0.8, < 0.10)
@@ -423,15 +447,24 @@ GEM
multi_json (~> 1.7)
omniauth (~> 1.1)
omniauth-oauth (~> 1.0)
+ omniauth-cas3 (1.1.3)
+ addressable (~> 2.3)
+ nokogiri (~> 1.6.6)
+ omniauth (~> 1.2)
+ omniauth-facebook (3.0.0)
+ omniauth-oauth2 (~> 1.2)
omniauth-github (1.1.2)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.1)
- omniauth-gitlab (1.0.0)
+ omniauth-gitlab (1.0.1)
omniauth (~> 1.0)
omniauth-oauth2 (~> 1.0)
- omniauth-google-oauth2 (0.2.6)
- omniauth (> 1.0)
- omniauth-oauth2 (~> 1.1)
+ omniauth-google-oauth2 (0.2.10)
+ addressable (~> 2.3)
+ jwt (~> 1.0)
+ multi_json (~> 1.3)
+ omniauth (>= 1.1.1)
+ omniauth-oauth2 (~> 1.3.1)
omniauth-kerberos (0.3.0)
omniauth-multipassword
timfel-krb5-auth (~> 0.8)
@@ -455,26 +488,26 @@ GEM
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
- opennebula (4.12.1)
+ opennebula (4.14.2)
json
nokogiri
rbvmomi
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
- paranoia (2.1.3)
+ paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.2.2.6)
+ parser (2.2.3.0)
ast (>= 1.1, < 3.0)
- pg (0.18.2)
- poltergeist (1.6.0)
+ pg (0.18.4)
+ poltergeist (1.8.1)
capybara (~> 2.1)
cliver (~> 0.3.1)
multi_json (~> 1.0)
websocket-driver (>= 0.2.0)
posix-spawn (0.3.11)
- powerpack (0.0.9)
- pry (0.10.1)
+ powerpack (0.1.1)
+ pry (0.10.3)
coderay (~> 1.1.0)
method_source (~> 0.8.1)
slop (~> 3.4)
@@ -483,21 +516,15 @@ GEM
pyu-ruby-sasl (0.0.3.3)
quiet_assets (1.0.3)
railties (>= 3.1, < 5.0)
- rack (1.5.5)
+ rack (1.6.4)
rack-accept (0.4.5)
rack (>= 0.4)
- rack-attack (4.3.0)
+ rack-attack (4.3.1)
rack
rack-cors (0.4.0)
- rack-lineprof (0.0.3)
- rack (~> 1.5)
- rblineprof (~> 0.3.6)
- term-ansicolor (~> 1.3)
- rack-mini-profiler (0.9.7)
- rack (>= 1.1.3)
rack-mount (0.8.3)
rack (>= 1.0.0)
- rack-oauth2 (1.0.10)
+ rack-oauth2 (1.2.1)
activesupport (>= 2.3)
attr_required (>= 0.0.5)
httpclient (>= 2.4)
@@ -507,28 +534,35 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rails (4.1.12)
- actionmailer (= 4.1.12)
- actionpack (= 4.1.12)
- actionview (= 4.1.12)
- activemodel (= 4.1.12)
- activerecord (= 4.1.12)
- activesupport (= 4.1.12)
+ rails (4.2.4)
+ actionmailer (= 4.2.4)
+ actionpack (= 4.2.4)
+ actionview (= 4.2.4)
+ activejob (= 4.2.4)
+ activemodel (= 4.2.4)
+ activerecord (= 4.2.4)
+ activesupport (= 4.2.4)
bundler (>= 1.3.0, < 2.0)
- railties (= 4.1.12)
- sprockets-rails (~> 2.0)
- rails-observers (0.1.2)
- activemodel (~> 4.0)
- railties (4.1.12)
- actionpack (= 4.1.12)
- activesupport (= 4.1.12)
+ railties (= 4.2.4)
+ sprockets-rails
+ rails-deprecated_sanitizer (1.0.3)
+ activesupport (>= 4.2.0.alpha)
+ rails-dom-testing (1.0.7)
+ activesupport (>= 4.2.0.beta, < 5.0)
+ nokogiri (~> 1.6.0)
+ rails-deprecated_sanitizer (>= 1.0.1)
+ rails-html-sanitizer (1.0.2)
+ loofah (~> 2.0)
+ railties (4.2.4)
+ actionpack (= 4.2.4)
+ activesupport (= 4.2.4)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.0.0)
raindrops (0.15.0)
rake (10.4.2)
raphael-rails (2.1.2)
- rb-fsevent (0.9.5)
+ rb-fsevent (0.9.6)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rblineprof (0.3.6)
@@ -539,14 +573,16 @@ GEM
trollop
rdoc (3.12.2)
json (~> 1.4)
+ recaptcha (1.0.2)
+ json
redcarpet (3.3.3)
- redis (3.2.1)
- redis-actionpack (4.0.0)
+ redis (3.2.2)
+ redis-actionpack (4.0.1)
actionpack (~> 4)
redis-rack (~> 1.5.0)
redis-store (~> 1.1.0)
- redis-activesupport (4.1.1)
- activesupport (~> 4)
+ redis-activesupport (4.1.5)
+ activesupport (>= 3, < 5)
redis-store (~> 1.1.0)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
@@ -557,13 +593,13 @@ GEM
redis-actionpack (~> 4)
redis-activesupport (~> 4)
redis-store (~> 1.1.0)
- redis-store (1.1.6)
+ redis-store (1.1.7)
redis (>= 2.2)
- request_store (1.2.0)
- rerun (0.10.0)
- listen (~> 2.7, >= 2.7.3)
- responders (1.1.2)
- railties (>= 3.2, < 4.2)
+ request_store (1.2.1)
+ rerun (0.11.0)
+ listen (~> 3.0)
+ responders (2.1.0)
+ railties (>= 4.2.0, < 5)
rest-client (1.8.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 3.0)
@@ -596,35 +632,38 @@ GEM
rspec-mocks (~> 3.3.0)
rspec-support (~> 3.3.0)
rspec-support (3.3.0)
- rubocop (0.28.0)
+ rubocop (0.35.1)
astrolabe (~> 1.3)
- parser (>= 2.2.0.pre.7, < 3.0)
- powerpack (~> 0.0.6)
+ parser (>= 2.2.3.0, < 3.0)
+ powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
- ruby-progressbar (~> 1.4)
+ ruby-progressbar (~> 1.7)
+ tins (<= 1.6.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
ruby-progressbar (1.7.5)
ruby-saml (1.0.0)
nokogiri (>= 1.5.10)
uuid (~> 2.3)
- ruby2ruby (2.1.4)
+ ruby2ruby (2.2.0)
ruby_parser (~> 3.1)
sexp_processor (~> 4.0)
- ruby_parser (3.5.0)
+ ruby_parser (3.7.2)
sexp_processor (~> 4.1)
rubyntlm (0.5.2)
rubypants (0.2.0)
- rugged (0.22.2)
+ rufus-scheduler (3.1.10)
+ rugged (0.23.3)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
- sass (3.2.19)
- sass-rails (4.0.5)
+ sass (3.4.20)
+ sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
- sass (~> 3.2.2)
- sprockets (~> 2.8, < 3.0)
- sprockets-rails (~> 2.0)
+ sass (~> 3.1)
+ sprockets (>= 2.8, < 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
+ tilt (>= 1.1, < 3)
sawyer (0.6.0)
addressable (~> 2.3.5)
faraday (~> 0.8, < 0.10)
@@ -642,16 +681,15 @@ GEM
rack
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
- sidekiq (3.3.0)
- celluloid (>= 0.16.0)
- connection_pool (>= 2.0.0)
- json
- redis (>= 3.0.6)
- redis-namespace (>= 1.3.1)
- sidetiq (0.6.3)
- celluloid (>= 0.14.1)
- ice_cube (= 0.11.1)
- sidekiq (>= 3.0.0)
+ sidekiq (4.0.1)
+ concurrent-ruby (~> 1.0)
+ connection_pool (~> 2.2, >= 2.2.0)
+ json (~> 1.0)
+ redis (~> 3.2, >= 3.2.1)
+ sidekiq-cron (0.4.0)
+ redis-namespace (>= 1.5.2)
+ rufus-scheduler (>= 2.0.24)
+ sidekiq (>= 4.0.0)
simple_oauth (0.1.9)
simplecov (0.10.0)
docile (~> 1.1.0)
@@ -664,6 +702,9 @@ GEM
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
@@ -685,12 +726,18 @@ GEM
multi_json (~> 1.0)
rack (~> 1.0)
tilt (~> 1.1, != 1.3.0)
- sprockets-rails (2.3.2)
+ sprockets-rails (2.3.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
- stamp (0.5.0)
- state_machine (1.2.0)
+ stamp (0.6.0)
+ state_machines (0.4.0)
+ state_machines-activemodel (0.3.0)
+ activemodel (~> 4.1)
+ state_machines (>= 0.4.0)
+ state_machines-activerecord (0.3.0)
+ activerecord (~> 4.1)
+ state_machines-activemodel (>= 0.3.0)
stringex (2.5.2)
systemu (2.6.5)
task_list (1.0.2)
@@ -699,20 +746,19 @@ GEM
railties (>= 3.2.5, < 5)
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)
- test_after_commit (0.2.7)
+ test_after_commit (0.4.2)
activerecord (>= 3.2)
- thin (1.6.3)
+ thin (1.6.4)
daemons (~> 1.0, >= 1.0.9)
- eventmachine (~> 1.0)
+ eventmachine (~> 1.0, >= 1.0.4)
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.5)
tilt (1.4.1)
- timers (4.0.4)
- hitimes
timfel-krb5-auth (0.8.3)
tinder (1.10.1)
eventmachine (~> 1.0)
@@ -736,7 +782,7 @@ GEM
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
- underscore-rails (1.4.4)
+ underscore-rails (1.8.3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.1)
@@ -744,9 +790,9 @@ GEM
kgio (~> 2.6)
rack
raindrops (~> 0.7)
- unicorn-worker-killer (0.4.3)
+ unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
- unicorn (~> 4)
+ unicorn (>= 4, < 6)
uniform_notifier (1.9.0)
uuid (2.3.8)
macaddr (~> 1.0)
@@ -756,12 +802,17 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- warden (1.2.3)
+ warden (1.2.4)
rack (>= 1.0)
+ web-console (2.2.1)
+ activemodel (>= 4.0)
+ binding_of_caller (>= 0.7.2)
+ railties (>= 4.0)
+ sprockets-rails (>= 2.0, < 4.0)
webmock (1.21.0)
addressable (>= 2.3.6)
crack (>= 0.3.2)
- websocket-driver (0.6.2)
+ websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
wikicloth (0.8.1)
@@ -777,88 +828,97 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.2.9)
ace-rails-ap (~> 2.0.1)
- active_record_query_trace
activerecord-deprecated_finders (~> 1.0.3)
activerecord-session_store (~> 0.1.0)
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
+ allocations (~> 1.0)
annotate (~> 2.6.0)
- asana (~> 0.0.6)
+ asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
attr_encrypted (~> 1.3.4)
awesome_print (~> 1.2.0)
+ babosa (~> 1.0.2)
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.0)
- brakeman (= 3.0.1)
+ bootstrap-sass (~> 3.3.0)
+ brakeman (~> 3.1.0)
browser (~> 1.0.0)
bullet
+ bundler-audit
byebug
- cal-heatmap-rails (~> 0.0.1)
+ cal-heatmap-rails (~> 3.5.0)
capybara (~> 2.4.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.9.0)
- charlock_holmes (~> 0.6.9.4)
+ charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
- colored (~> 1.2)
- colorize (~> 0.5.8)
+ colorize (~> 0.7.0)
+ connection_pool (~> 2.0)
coveralls (~> 0.8.2)
- creole (~> 0.3.6)
- d3_rails (~> 3.5.5)
+ creole (~> 0.5.0)
+ d3_rails (~> 3.5.0)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
- devise (~> 3.5.2)
+ devise (~> 3.5.3)
devise-async (~> 0.9.0)
devise-two-factor (~> 2.0.0)
diffy (~> 3.0.3)
- doorkeeper (~> 2.1.3)
+ doorkeeper (~> 2.2.0)
dropzonejs-rails (~> 0.7.1)
email_reply_parser (~> 0.5.8)
email_spec (~> 1.6.0)
- enumerize (~> 0.7.0)
factory_girl_rails (~> 4.3.0)
ffaker (~> 2.0.0)
+ flay
+ flog
fog (~> 1.25.0)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2)
+ github-linguist (~> 4.7.0)
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab-linguist (~> 3.0.1)
- gitlab_emoji (~> 0.1)
- gitlab_git (~> 7.2.19)
+ gitlab_emoji (~> 0.2.0)
+ gitlab_git (~> 7.2.20)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
- gollum-lib (~> 4.0.2)
- gon (~> 5.0.0)
- grape (~> 0.6.1)
+ gollum-lib (~> 4.1.0)
+ gon (~> 6.0.1)
+ grape (~> 0.13.0)
grape-entity (~> 0.4.2)
haml-rails (~> 0.9.0)
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
- jquery-atwho-rails (~> 1.0.0)
- jquery-rails (~> 3.1.3)
+ influxdb (~> 0.2)
+ jquery-atwho-rails (~> 1.3.2)
+ jquery-rails (~> 4.0.0)
jquery-scrollto-rails (~> 1.4.3)
- jquery-turbolinks (~> 2.0.1)
- jquery-ui-rails (~> 4.2.1)
+ jquery-turbolinks (~> 2.1.0)
+ jquery-ui-rails (~> 5.0.0)
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
mail_room (~> 0.6.1)
+ method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
nested_form (~> 0.3.2)
+ net-ssh (~> 3.0.1)
newrelic-grape
newrelic_rpm (~> 3.9.4.245)
- nprogress-rails (~> 0.1.2.3)
+ nokogiri (= 1.6.7.1)
+ nprogress-rails (~> 0.1.6.7)
oauth2 (~> 1.0.0)
octokit (~> 3.7.0)
omniauth (~> 1.2.2)
omniauth-bitbucket (~> 0.0.2)
+ omniauth-cas3 (~> 1.1.2)
+ omniauth-facebook (~> 3.0.0)
omniauth-github (~> 1.1.1)
omniauth-gitlab (~> 1.0.0)
omniauth-google-oauth2 (~> 0.2.0)
@@ -870,35 +930,39 @@ DEPENDENCIES
org-ruby (~> 0.9.12)
paranoia (~> 2.0)
pg (~> 0.18.2)
- poltergeist (~> 1.6.0)
+ poltergeist (~> 1.8.1)
pry-rails
quiet_assets (~> 1.0.2)
- rack-attack (~> 4.3.0)
+ rack-attack (~> 4.3.1)
rack-cors (~> 0.4.0)
- rack-lineprof
- rack-mini-profiler (~> 0.9.0)
- rack-oauth2 (~> 1.0.5)
- rails (= 4.1.12)
+ rack-oauth2 (~> 1.2.1)
+ rails (= 4.2.4)
+ rails-deprecated_sanitizer (~> 1.0.3)
raphael-rails (~> 2.1.2)
+ rblineprof
rdoc (~> 3.6)
+ recaptcha
redcarpet (~> 3.3.3)
+ redis-namespace
redis-rails (~> 4.0.0)
request_store (~> 1.2.0)
- rerun (~> 0.10.0)
+ rerun (~> 0.11.0)
+ responders (~> 2.0)
+ rouge (~> 1.10.1)
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.3.0)
- rubocop (~> 0.28.0)
+ rubocop (~> 0.35.0)
ruby-fogbugz (~> 0.2.1)
sanitize (~> 2.0)
- sass-rails (~> 4.0.5)
+ sass-rails (~> 5.0.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
settingslogic (~> 2.0.9)
sham_rack
shoulda-matchers (~> 2.8.0)
- sidekiq (= 3.3.0)
- sidetiq (~> 0.6.3)
+ sidekiq (~> 4.0)
+ sidekiq-cron (~> 0.4.0)
simplecov (~> 0.10.0)
sinatra (~> 1.4.4)
six (~> 0.2.0)
@@ -909,22 +973,23 @@ DEPENDENCIES
spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 2.12.3)
- stamp (~> 0.5.0)
- state_machine (~> 1.2.0)
+ stamp (~> 0.6.0)
+ state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2)
teaspoon (~> 1.0.0)
teaspoon-jasmine (~> 2.2.0)
- test_after_commit (~> 0.2.2)
+ test_after_commit (~> 0.4.2)
thin (~> 1.6.1)
tinder (~> 1.10.0)
turbolinks (~> 2.5.0)
uglifier (~> 2.7.2)
- underscore-rails (~> 1.4.4)
+ underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
unicorn (~> 4.8.2)
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.0.0)
virtus (~> 1.0.1)
+ web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
diff --git a/LICENSE b/LICENSE
index d8cb29f3638..1dc1bdb7411 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2015 GitLab B.V.
+Copyright (c) 2011-2016 GitLab B.V.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/PROCESS.md b/PROCESS.md
index 9f4b708d2b5..5f4d67bc10e 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -8,7 +8,7 @@ Below we describe the contributing process to GitLab for two reasons. So that co
### Issue team
- Looks for issues without [workflow labels](#how-we-handle-issues) and triages issue
-- Closes invalid issues with a comment (duplicates, [feature requests](#feature-requests), [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.)
+- Closes invalid issues with a comment (duplicates, [fixed in newer version](#issue-fixed-in-newer-version), [issue report for old version](#issue-report-for-old-version), not a problem in GitLab, etc.)
- Asks for feedback from issue reporter ([invalid issue reports](#improperly-formatted-issue), [format code](#code-format), etc.)
- Monitors all issues for feedback (but especially ones commented on since automatically watching them)
- Closes issues with no feedback from the reporter for two weeks
@@ -34,13 +34,21 @@ The most important thing is making sure valid issues receive feedback from the d
## Workflow labels
-Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
+Workflow labels are purposely not very detailed since that would be hard to keep updated as you would need to re-evaluate them after every comment. We optionally use functional labels on demand when want to group related issues to get an overview (for example all issues related to RVM, to tackle them in one go) and to add details to the issue.
- *Awaiting feedback*: Feedback pending from the reporter
- *Awaiting confirmation of fix*: The issue should already be solved in **master** (generally you can avoid this workflow item and just close the issue right away)
- *Attached MR*: There is a MR attached and the discussion should happen there
- We need to let issues stay in sync with the MR's. We can do this with a "Closing #XXXX" or "Fixes #XXXX" comment in the MR. We can't close the issue when there is a merge request because sometimes a MR is not good and we just close the MR, then the issue must stay.
-- *Awaiting developer action/feedback*: Issue needs to be fixed or clarified by a developer
+- *Developer*: needs help from a developer
+- *UX* needs needs help from a UX designer
+- *Frontend* needs help from a Front-end engineer
+- *Graphics* needs help from a Graphics designer
+- *up-for-grabs* is an issue suitable for first-time contributors, of reasonable difficulty and size. Not exclusive with other labels.
+- *feature proposal* is a proposal for a new feature for GitLab. People are encouraged to vote
+in support or comment for further detail. Do not use `feature request`.
+
+Example workflow: when a UX designer provided a design but it needs frontend work they remove the UX label and add the frontend label.
## Functional labels
@@ -56,7 +64,6 @@ If an issue is complex and needs the attention of a specific person, assignment
- Bright orange `#eb6420`: workflow labels for core team members (attached MR, awaiting developer action/feedback)
- Light blue `#82C5FF`: functional labels
- Green labels `#009800`: issues that can generally be ignored. For example, issues given the following labels normally can be closed immediately:
- - Feature request (see copy & paste response: [Feature requests](#feature-requests))
- Support (see copy & paste response: [Support requests and configuration questions](#support-requests-and-configuration-questions)
## Be kind
@@ -69,10 +76,6 @@ Be kind to people trying to contribute. Be aware that people may be a non-native
Thanks for the issue report. Please reformat your issue to conform to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
-### Feature requests
-
-Thank you for your interest in improving GitLab. We don't use the issue tracker for feature requests. Things that are wrong but are not a regression compared to older versions of GitLab are considered feature requests and not issues. Please use the \[feature request forum\]\(http://feedback.gitlab.com/) for this purpose or create a merge request implementing this feature. Have a look at the \[contribution guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md) for more information.
-
### Issue report for old version
Thanks for the issue report but we only support issues for the latest stable version of GitLab. I'm closing this issue but if you still experience this problem in the latest stable version, please open a new issue (but also reference the old issue(s)). Make sure to also include the necessary debugging information conforming to the issue tracker guidelines found in our \[contributing guidelines\]\(https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker-guidelines).
@@ -107,7 +110,12 @@ This merge request has been closed because a request for more information has no
### Accepting merge requests
-Is there a request on [the feature request forum](http://feedback.gitlab.com/forums/176466-general) that is similar to this? If so, can you make a comment with a link to it? Please be aware that new functionality that is not marked [accepting merge/pull requests](http://feedback.gitlab.com/forums/176466-general/status/796455) on the forum might not make it into GitLab. You might be asked to make changes and even after implementing them your feature might still be declined. If you want to reduce the chance of this happening please have a discussion in the forum first.
+Is there an issue on the [issue tracker](https://gitlab.com/gitlab-org/gitlab-ce/issues)
+that is similar to this?
+Could you please link it here?
+Please be aware that new functionality that is not marked
+[accepting merge requests](https://gitlab.com/gitlab-org/gitlab-ce/issues?milestone_id=&scope=all&sort=created_desc&state=opened&utf8=%E2%9C%93&assignee_id=&author_id=&milestone_title=&label_name=Accepting+Merge+Requests)
+might not make it into GitLab.
### Only accepting merge requests with green tests
@@ -115,3 +123,10 @@ We can only accept a merge request if all the tests are green. I've just
restarted the build. When the tests are still not passing after this restart and
you're sure that is does not have anything to do with your code changes, please
rebase with master to see if that solves the issue.
+
+### Closing down the issue tracker on GitHub
+
+We are currently in the process of closing down the issue tracker on GitHub, to
+prevent duplication with the GitLab.com issue tracker.
+Since this is an older issue I'll be closing this for now. If you think this is
+still an issue I encourage you to open it on the \[GitLab.com issue tracker\](https://gitlab.com/gitlab-org/gitlab-ce/issues).
diff --git a/Procfile b/Procfile
index 08880b9c425..9cfdee7040f 100644
--- a/Procfile
+++ b/Procfile
@@ -1,3 +1,7 @@
+# For DEVELOPMENT only. Production uses Runit in
+# https://gitlab.com/gitlab-org/omnibus-gitlab or the init scripts in
+# lib/support/init.d, which call scripts in bin/ .
+#
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
-worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
+worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
diff --git a/README.md b/README.md
index 52e2d977620..3ec1d4a776c 100644
--- a/README.md
+++ b/README.md
@@ -27,8 +27,6 @@ There are two editions of GitLab:
- GitLab Community Edition (CE) is available freely under the MIT Expat license.
- GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/features/#compare) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/).
-Included with the GitLab Omnibus Packages is [GitLab CI](https://about.gitlab.com/gitlab-ci/) that can easily build, test and deploy code.
-
## Website
On [about.gitlab.com](https://about.gitlab.com/) you can find more information about:
@@ -71,7 +69,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
- Ruby (MRI) 2.1
- Git 1.7.10+
-- Redis 2.4+
+- Redis 2.8+
- MySQL or PostgreSQL
For more information please see the [architecture documentation](http://doc.gitlab.com/ce/development/architecture.html).
@@ -82,7 +80,7 @@ There are a lot of [third-party applications integrating with GitLab](https://ab
## GitLab release cycle
-Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). For more information about the release process see the [release documentation](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/release). Features that will likely be in the next releases can be found on the [feature request forum](http://feedback.gitlab.com/forums/176466-general) with the status [started](http://feedback.gitlab.com/forums/176466-general/status/796456) and [completed](http://feedback.gitlab.com/forums/176466-general/status/796457).
+For more information about the release process see the [release documentation](http://doc.gitlab.com/ce/release/).
## Upgrading
diff --git a/VERSION b/VERSION
index a2264f05f50..ce669730119 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.1.0.pre
+8.4.0.pre
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
index cb89a2d171e..9c9b5cb7f03 100755..100644
--- a/app/assets/fonts/SourceSansPro-Black.ttf
+++ b/app/assets/fonts/SourceSansPro-Black.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf
new file mode 100644
index 00000000000..294ce5abe8f
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
index 5d65c93242f..5d65c93242f 100755..100644
--- a/app/assets/fonts/SourceSansPro-Bold.ttf
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf
new file mode 100644
index 00000000000..3decd130070
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
index bb4176c6fff..253eafa3783 100755..100644
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
new file mode 100644
index 00000000000..00d7e9a7aa8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf
new file mode 100644
index 00000000000..f7af5377595
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
index 83a0a336661..83a0a336661 100755..100644
--- a/app/assets/fonts/SourceSansPro-Light.ttf
+++ b/app/assets/fonts/SourceSansPro-Light.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf
new file mode 100644
index 00000000000..f18827985ef
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
index 44486cdc670..44486cdc670 100755..100644
--- a/app/assets/fonts/SourceSansPro-Regular.ttf
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
index 86b00c067e0..86b00c067e0 100755..100644
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
new file mode 100644
index 00000000000..13d66a1fc45
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
Binary files differ
diff --git a/app/assets/images/auth_buttons/facebook_64.png b/app/assets/images/auth_buttons/facebook_64.png
new file mode 100644
index 00000000000..1f1a80d7368
--- /dev/null
+++ b/app/assets/images/auth_buttons/facebook_64.png
Binary files differ
diff --git a/app/assets/images/brand_logo.png b/app/assets/images/brand_logo.png
deleted file mode 100644
index 9c564bb6141..00000000000
--- a/app/assets/images/brand_logo.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/emoji.png b/app/assets/images/emoji.png
new file mode 100644
index 00000000000..a8ad7b6eab6
--- /dev/null
+++ b/app/assets/images/emoji.png
Binary files differ
diff --git a/app/assets/images/gitlab_logo.png b/app/assets/images/gitlab_logo.png
new file mode 100644
index 00000000000..0c157546b9c
--- /dev/null
+++ b/app/assets/images/gitlab_logo.png
Binary files differ
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
index 60021d5ac47..7d89da97e11 100644
--- a/app/assets/images/icon-link.png
+++ b/app/assets/images/icon-link.png
Binary files differ
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 9e5d594c861..746fa3cea87 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -2,6 +2,8 @@
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"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -44,6 +46,35 @@
).done (namespaces) ->
callback(namespaces)
+ # Return projects list. Filtered by query
+ projects: (query, callback) ->
+ url = Api.buildUrl(Api.projects_path)
+
+ $.ajax(
+ url: url
+ data:
+ private_token: gon.api_token
+ search: query
+ per_page: 20
+ dataType: "json"
+ ).done (projects) ->
+ callback(projects)
+
+ # Return group projects list. Filtered by query
+ groupProjects: (group_id, query, callback) ->
+ url = Api.buildUrl(Api.group_projects_path)
+ url = url.replace(':id', group_id)
+
+ $.ajax(
+ url: url
+ data:
+ private_token: gon.api_token
+ search: query
+ per_page: 20
+ dataType: "json"
+ ).done (projects) ->
+ callback(projects)
+
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 945ffb660e6..b9b095e004a 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -5,17 +5,17 @@
# the compiled file.
#
#= require jquery
-#= require jquery.ui.all
+#= require jquery-ui
#= require jquery_ujs
#= require jquery.cookie
#= require jquery.endless-scroll
#= require jquery.highlight
-#= require jquery.history
#= require jquery.waitforimages
#= require jquery.atwho
#= require jquery.scrollTo
-#= require jquery.blockUI
#= require jquery.turbolinks
+#= require d3
+#= require cal-heatmap
#= require turbolinks
#= require autosave
#= require bootstrap
@@ -27,7 +27,6 @@
#= require branch-graph
#= require ace/ace
#= require ace/ext-searchbox
-#= require d3
#= require underscore
#= require nprogress
#= require nprogress-turbolinks
@@ -39,7 +38,6 @@
#= require shortcuts_dashboard_navigation
#= require shortcuts_issuable
#= require shortcuts_network
-#= require cal-heatmap
#= require jquery.nicescroll.min
#= require_tree .
@@ -135,17 +133,25 @@ $ ->
), 1
# Initialize tooltips
- $('body').tooltip({
- selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a'
+ $('body').tooltip(
+ selector: '.has_tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
- if $el.attr('id') == 'js-shortcuts-home'
- # Place the logo tooltip on the right when collapsed, bottom when expanded
- $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom'
- else
- # Otherwise use the data-placement attribute, or 'bottom' if undefined
- $el.data('placement') or 'bottom'
- })
+ $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', ->
diff --git a/app/assets/javascripts/awards_handler.coffee b/app/assets/javascripts/awards_handler.coffee
new file mode 100644
index 00000000000..619abb1fb07
--- /dev/null
+++ b/app/assets/javascripts/awards_handler.coffee
@@ -0,0 +1,166 @@
+class @AwardsHandler
+ constructor: (@post_emoji_url, @noteable_type, @noteable_id, @aliases) ->
+ $(".add-award").click (event)->
+ event.stopPropagation()
+ event.preventDefault()
+ $(".emoji-menu").show()
+
+ $("html").click ->
+ if !$(event.target).closest(".emoji-menu").length
+ if $(".emoji-menu").is(":visible")
+ $(".emoji-menu").hide()
+
+ @renderFrequentlyUsedBlock()
+ @setupSearch()
+
+ addAward: (emoji) ->
+ emoji = @normilizeEmojiName(emoji)
+ @postEmoji emoji, =>
+ @addAwardToEmojiBar(emoji)
+
+ $(".emoji-menu").hide()
+
+ addAwardToEmojiBar: (emoji) ->
+ @addEmojiToFrequentlyUsedList(emoji)
+
+ emoji = @normilizeEmojiName(emoji)
+ if @exist(emoji)
+ if @isActive(emoji)
+ @decrementCounter(emoji)
+ else
+ counter = @findEmojiIcon(emoji).siblings(".counter")
+ counter.text(parseInt(counter.text()) + 1)
+ counter.parent().addClass("active")
+ @addMeToAuthorList(emoji)
+ else
+ @createEmoji(emoji)
+
+ exist: (emoji) ->
+ @findEmojiIcon(emoji).length > 0
+
+ isActive: (emoji) ->
+ @findEmojiIcon(emoji).parent().hasClass("active")
+
+ decrementCounter: (emoji) ->
+ counter = @findEmojiIcon(emoji).siblings(".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")
+ else
+ emojiIcon.tooltip("destroy")
+ emojiIcon.remove()
+
+ removeMeFromAuthorList: (emoji) ->
+ award_block = @findEmojiIcon(emoji).parent()
+ authors = award_block.attr("data-original-title").split(", ")
+ authors = _.without(authors, "me").join(", ")
+ award_block.attr("title", authors)
+ @resetTooltip(award_block)
+
+ addMeToAuthorList: (emoji) ->
+ award_block = @findEmojiIcon(emoji).parent()
+ authors = award_block.attr("data-original-title").split(", ")
+ authors.push("me")
+ award_block.attr("title", authors.join(", "))
+ @resetTooltip(award_block)
+
+ 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
+
+
+ createEmoji: (emoji) ->
+ emojiCssClass = @resolveNameToCssClass(emoji)
+
+ nodes = []
+ nodes.push("<div class='award active' title='me'>")
+ nodes.push("<div class='icon emoji-icon #{emojiCssClass}' data-emoji='#{emoji}'></div>")
+ nodes.push("<div class='counter'>1</div>")
+ nodes.push("</div>")
+
+ emoji_node = $(nodes.join("\n")).insertBefore(".awards-controls").find(".emoji-icon").data("emoji", emoji)
+
+ $(".award").tooltip()
+
+ resolveNameToCssClass: (emoji) ->
+ emoji_icon = $(".emoji-menu-content [data-emoji='#{emoji}']")
+
+ if emoji_icon.length > 0
+ unicodeName = emoji_icon.data("unicode-name")
+ else
+ # Find by alias
+ unicodeName = $(".emoji-menu-content [data-aliases*=':#{emoji}:']").data("unicode-name")
+
+ "emoji-#{unicodeName}"
+
+ postEmoji: (emoji, callback) ->
+ $.post @post_emoji_url, { note: {
+ note: ":#{emoji}:"
+ noteable_type: @noteable_type
+ noteable_id: @noteable_id
+ }},(data) ->
+ if data.ok
+ callback.call()
+
+ findEmojiIcon: (emoji) ->
+ $(".award [data-emoji='#{emoji}']")
+
+ scrollToAwards: ->
+ $('body, html').animate({
+ scrollTop: $('.awards').offset().top - 80
+ }, 200)
+
+ normilizeEmojiName: (emoji) ->
+ @aliases[emoji] || emoji
+
+ addEmojiToFrequentlyUsedList: (emoji) ->
+ frequently_used_emojis = @getFrequentlyUsedEmojis()
+ frequently_used_emojis.push(emoji)
+ $.cookie('frequently_used_emojis', frequently_used_emojis.join(","), { expires: 365 })
+
+ getFrequentlyUsedEmojis: ->
+ frequently_used_emojis = ($.cookie('frequently_used_emojis') || "").split(",")
+ _.compact(_.uniq(frequently_used_emojis))
+
+ renderFrequentlyUsedBlock: ->
+ if $.cookie('frequently_used_emojis')
+ frequently_used_emojis = @getFrequentlyUsedEmojis()
+
+ ul = $("<ul>")
+
+ for emoji in frequently_used_emojis
+ do (emoji) ->
+ $(".emoji-menu-content [data-emoji='#{emoji}']").closest("li").clone().appendTo(ul)
+
+ $("input.emoji-search").after(ul).after($("<h5>").text("Frequently used"))
+
+ setupSearch: ->
+ $("input.emoji-search").keyup (ev) =>
+ term = $(ev.target).val()
+
+ # Clean previous search results
+ $("ul.emoji-search,h5.emoji-search").remove()
+
+ if term
+ # Generate a search result block
+ h5 = $("<h5>").text("Search results").addClass("emoji-search")
+ found_emojis = @searchEmojis(term).show()
+ ul = $("<ul>").addClass("emoji-search").append(found_emojis)
+ $(".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()
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
index 5b604adbbb1..9df932817f6 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
@@ -23,18 +23,6 @@ class @BlobFileDropzone
init: ->
this.on 'addedfile', (file) ->
$('.dropzone-alerts').html('').hide()
- commit_message = form.find('#commit_message')[0]
-
- if /^Upload/.test(commit_message.placeholder)
- commit_message.placeholder = 'Upload ' + file.name
-
- return
-
- this.on 'removedfile', (file) ->
- commit_message = form.find('#commit_message')[0]
-
- if /^Upload/.test(commit_message.placeholder)
- commit_message.placeholder = 'Upload new file'
return
@@ -47,8 +35,9 @@ class @BlobFileDropzone
return
this.on 'sending', (file, xhr, formData) ->
- formData.append('new_branch', form.find('#new_branch').val())
- formData.append('commit_message', form.find('#commit_message').val())
+ formData.append('target_branch', form.find('.js-target-branch').val())
+ formData.append('create_merge_request', form.find('.js-create-merge-request').val())
+ formData.append('commit_message', form.find('.js-commit-message').val())
return
# Override behavior of adding error underneath preview
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 050888f9c15..f6bf836f19f 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -11,10 +11,10 @@ class @EditBlob
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
- $(".js-commit-button").click ->
- $("#file-content").val editor.getValue()
- $(".file-editor form").submit()
- return false
+ # Before a form submission, move the content from the Ace editor into the
+ # submitted textarea
+ $('form').submit ->
+ $("#file-content").val(editor.getValue())
editModePanes = $(".js-edit-mode-pane")
editModeLinks = $(".js-edit-mode a")
diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee
index 1f36a53f191..68c5e5195e3 100644
--- a/app/assets/javascripts/blob/new_blob.js.coffee
+++ b/app/assets/javascripts/blob/new_blob.js.coffee
@@ -11,10 +11,10 @@ class @NewBlob
if ace_mode
editor.getSession().setMode "ace/mode/" + ace_mode
- $(".js-commit-button").click ->
- $("#file-content").val editor.getValue()
- $(".file-editor form").submit()
- return false
+ # Before a form submission, move the content from the Ace editor into the
+ # submitted textarea
+ $('form').submit ->
+ $("#file-content").val(editor.getValue())
editor: ->
return @editor
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 4c4bc3d66ed..d80e0e716ce 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -1,9 +1,4 @@
class @Calendar
- options =
- month: "short"
- day: "numeric"
- year: "numeric"
-
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap()
cal.init
@@ -25,7 +20,7 @@ class @Calendar
30
]
legendCellPadding: 3
- cellSize: $('.user-calendar').width() / 80
+ cellSize: $('.user-calendar').width() / 73
onClick: (date, count) ->
formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()
$.ajax
diff --git a/app/assets/javascripts/ci/build.coffee b/app/assets/javascripts/ci/build.coffee
index c30859b484b..44d5ddb7d95 100644
--- a/app/assets/javascripts/ci/build.coffee
+++ b/app/assets/javascripts/ci/build.coffee
@@ -22,7 +22,7 @@ class CiBuild
# Only valid for runnig build when output changes during time
#
CiBuild.interval = setInterval =>
- if window.location.href is build_url
+ if window.location.href.split("#").first() is build_url
$.ajax
url: build_url
dataType: "json"
@@ -31,7 +31,7 @@ class CiBuild
$('#build-trace code').html build.trace_html
$('#build-trace code').append '<i class="fa fa-refresh fa-spin"/>'
@checkAutoscroll()
- else
+ else if build.status != build_status
Turbolinks.visit build_url
, 4000
diff --git a/app/assets/javascripts/copy_to_clipboard.js.coffee b/app/assets/javascripts/copy_to_clipboard.js.coffee
new file mode 100644
index 00000000000..24301e01b10
--- /dev/null
+++ b/app/assets/javascripts/copy_to_clipboard.js.coffee
@@ -0,0 +1,37 @@
+#= require clipboard
+
+genericSuccess = (e) ->
+ showTooltip(e.trigger, 'Copied!')
+
+ # Clear the selection and blur the trigger so it loses its border
+ e.clearSelection()
+ $(e.trigger).blur()
+
+# Safari doesn't support `execCommand`, so instead we inform the user to
+# copy manually.
+#
+# See http://clipboardjs.com/#browser-support
+genericError = (e) ->
+ if /Mac/i.test(navigator.userAgent)
+ key = '&#8984;' # Command
+ else
+ key = 'Ctrl'
+
+ showTooltip(e.trigger, "Press #{key}-C to copy")
+
+showTooltip = (target, title) ->
+ $(target).
+ tooltip(
+ container: 'body'
+ html: 'true'
+ placement: 'auto bottom'
+ title: title
+ trigger: 'manual'
+ ).
+ tooltip('show').
+ one('mouseleave', -> $(this).tooltip('hide'))
+
+$ ->
+ clipboard = new Clipboard '[data-clipboard-target], [data-clipboard-text]'
+ clipboard.on 'success', genericSuccess
+ clipboard.on 'error', genericError
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 5bf0b302179..69e061ce6e9 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -28,6 +28,8 @@ class Dispatcher
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
new DropzoneInput($('.milestone-form'))
+ when 'groups:milestones:new'
+ new ZenMode()
when 'projects:compare:show'
new Diff()
when 'projects:issues:new','projects:issues:edit'
@@ -39,9 +41,15 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
new DropzoneInput($('.merge-request-form'))
new IssuableForm($('.merge-request-form'))
+ when 'projects:tags:new'
+ new ZenMode()
+ new DropzoneInput($('.tag-form'))
+ when 'projects:releases:edit'
+ new ZenMode()
+ new DropzoneInput($('.release-form'))
when 'projects:merge_requests:show'
new Diff()
- shortcut_handler = new ShortcutsIssuable()
+ shortcut_handler = new ShortcutsIssuable(true)
new ZenMode()
when "projects:merge_requests:diffs"
new Diff()
@@ -75,7 +83,7 @@ class Dispatcher
when 'projects:project_members:index'
new ProjectMembers()
new UsersSelect()
- when 'groups:new', 'groups:edit', 'admin:groups:edit'
+ when 'groups:new', 'groups:edit', 'admin:groups:edit', 'admin:groups:new'
new GroupAvatar()
when 'projects:tree:show'
new TreeView()
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 6f789e668af..30a35a04339 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -1,3 +1,5 @@
+#= require markdown_preview
+
class @DropzoneInput
constructor: (form) ->
Dropzone.autoDiscover = false
@@ -11,17 +13,14 @@ class @DropzoneInput
uploadProgress = $("<div class=\"div-dropzone-progress\"></div>")
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"
project_uploads_path = window.project_uploads_path or null
- markdown_preview_path = window.markdown_preview_path or null
max_file_size = gon.max_file_size or 10
form_textarea = $(form).find("textarea.markdown-area")
form_textarea.wrap "<div class=\"div-dropzone\"></div>"
form_textarea.on 'paste', (event) =>
handlePaste(event)
- form_textarea.on "input", ->
- hideReferencedUsers()
- form_textarea.on "blur", ->
- renderMarkdown()
+
+ $(form).setupMarkdownPreview()
form_dropzone = $(form).find('.div-dropzone')
form_dropzone.parent().addClass "div-dropzone-wrapper"
@@ -34,42 +33,6 @@ class @DropzoneInput
"opacity": 0
"display": "none"
- # Preview button
- $(document).off "click", ".js-md-preview-button"
- $(document).on "click", ".js-md-preview-button", (e) ->
- ###
- Shows the Markdown preview.
-
- Lets the server render GFM into Html and displays it.
- ###
- e.preventDefault()
- form = $(this).closest("form")
- # toggle tabs
- form.find(".js-md-write-button").parent().removeClass "active"
- form.find(".js-md-preview-button").parent().addClass "active"
-
- # toggle content
- form.find(".md-write-holder").hide()
- form.find(".md-preview-holder").show()
-
- renderMarkdown()
-
- # Write button
- $(document).off "click", ".js-md-write-button"
- $(document).on "click", ".js-md-write-button", (e) ->
- ###
- Shows the Markdown textarea.
- ###
- e.preventDefault()
- form = $(this).closest("form")
- # toggle tabs
- form.find(".js-md-write-button").parent().addClass "active"
- form.find(".js-md-preview-button").parent().removeClass "active"
-
- # toggle content
- form.find(".md-write-holder").show()
- form.find(".md-preview-holder").hide()
-
dropzone = form_dropzone.dropzone(
url: project_uploads_path
dictDefaultMessage: ""
@@ -136,41 +99,6 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
- hideReferencedUsers = ->
- referencedUsers = form.find(".referenced-users")
- referencedUsers.hide()
-
- renderReferencedUsers = (users) ->
- referencedUsers = form.find(".referenced-users")
-
- if referencedUsers.length
- if users.length >= 10
- referencedUsers.show()
- referencedUsers.find(".js-referenced-users-count").text users.length
- else
- referencedUsers.hide()
-
- renderMarkdown = ->
- preview = form.find(".js-md-preview")
- mdText = form.find(".markdown-area").val()
- if mdText.trim().length is 0
- preview.text "Nothing to preview."
- hideReferencedUsers()
- else
- preview.text "Loading..."
- $.ajax(
- type: "POST",
- url: markdown_preview_path,
- data: {
- text: mdText
- },
- dataType: "json"
- ).success (data) ->
- preview.html data.body
- preview.syntaxHighlight()
-
- renderReferencedUsers data.references.users
-
formatLink = (link) ->
text = "[#{link.alt}](#{link.url})"
text = "!#{text}" if link.is_image
diff --git a/app/assets/javascripts/flash.js.coffee b/app/assets/javascripts/flash.js.coffee
index b39ab0c4475..5de012e409f 100644
--- a/app/assets/javascripts/flash.js.coffee
+++ b/app/assets/javascripts/flash.js.coffee
@@ -1,12 +1,16 @@
class @Flash
constructor: (message, type)->
- flash = $(".flash-container")
- flash.html("")
+ @flash = $(".flash-container")
+ @flash.html("")
- $('<div/>',
+ innerDiv = $('<div/>',
class: "flash-#{type}",
text: message
- ).appendTo(".flash-container")
+ )
+ innerDiv.appendTo(".flash-container")
- flash.click -> $(@).fadeOut()
- flash.show()
+ @flash.click -> $(@).fadeOut()
+ @flash.show()
+
+ pinTo: (selector) ->
+ @flash.detach().appendTo(selector)
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index c4d3e619f5e..02232698bc2 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -5,9 +5,9 @@ class @IssuableContext
new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
- $(".context .inline-update").on "change", "select", ->
+ $(".issuable-sidebar .inline-update").on "change", "select", ->
$(this).submit()
- $(".context .inline-update").on "change", ".js-assignee", ->
+ $(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
$('.issuable-details').waitForImages ->
@@ -21,3 +21,9 @@ class @IssuableContext
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
+
+ $(".edit-link").click (e) ->
+ block = $(@).parents('.block')
+ block.find('.selectbox').show()
+ block.find('.value').hide()
+ block.find('.js-select2').select2("open")
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index 603a16da1ce..c256ec8f41b 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -1,3 +1,4 @@
+#= require flash
#= require jquery.waitforimages
#= require task_list
@@ -6,16 +7,47 @@ class @Issue
# Prevent duplicate event bindings
@disableTaskList()
- if $("a.btn-close").length
+ if $('a.btn-close').length
@initTaskList()
+ @initIssueBtnEventListeners()
initTaskList: ->
- $('.issue-details .js-task-list-container').taskList('enable')
- $(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList
+ $('.detail-page-description .js-task-list-container').taskList('enable')
+ $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
+
+ initIssueBtnEventListeners: ->
+ issueFailMessage = 'Unable to update this issue at this time.'
+ $('a.btn-close, a.btn-reopen').on 'click', (e) ->
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ $this = $(this)
+ isClose = $this.hasClass('btn-close')
+ $this.prop('disabled', true)
+ url = $this.attr('href')
+ $.ajax
+ type: 'PUT'
+ url: url,
+ error: (jqXHR, textStatus, errorThrown) ->
+ issueStatus = if isClose then 'close' else 'open'
+ new Flash(issueFailMessage, 'alert')
+ success: (data, textStatus, jqXHR) ->
+ if data.saved
+ $this.addClass('hidden')
+ if isClose
+ $('a.btn-reopen').removeClass('hidden')
+ $('div.status-box-closed').removeClass('hidden')
+ $('div.status-box-open').addClass('hidden')
+ else
+ $('a.btn-close').removeClass('hidden')
+ $('div.status-box-closed').addClass('hidden')
+ $('div.status-box-open').removeClass('hidden')
+ else
+ new Flash(issueFailMessage, 'alert')
+ $this.prop('disabled', false)
disableTaskList: ->
- $('.issue-details .js-task-list-container').taskList('disable')
- $(document).off 'tasklist:changed', '.issue-details .js-task-list-container'
+ $('.detail-page-description .js-task-list-container').taskList('disable')
+ $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
# TODO (rspeicher): Make the issue description inline-editable like a note so
# that we can re-use its form here
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index 40bb9e9cb0c..a0acf3028bf 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -15,13 +15,6 @@
$(this).html totalIssues + 1
else
$(this).html totalIssues - 1
- $("body").on "click", ".issues-other-filters .dropdown-menu a", ->
- $('.issues-list').block(
- message: null,
- overlayCSS:
- backgroundColor: '#DDD'
- opacity: .4
- )
reload: ->
Issues.initSelects()
@@ -29,7 +22,7 @@
$('#filter_issue_search').val($('#issue_search').val())
initSelects: ->
- $("select#update_status").select2(width: 'resolve', dropdownAutoWidth: true)
+ $("select#update_state_event").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_assignee_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#update_milestone_id").select2(width: 'resolve', dropdownAutoWidth: true)
$("select#label_name").select2(width: 'resolve', dropdownAutoWidth: true)
@@ -54,7 +47,7 @@
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.issues-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
@@ -65,7 +58,7 @@
success: (data) ->
$('.issues-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- History.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issues_url}, document.title, issues_url
Issues.reload()
dataType: "json"
diff --git a/app/assets/javascripts/logo.js.coffee b/app/assets/javascripts/logo.js.coffee
new file mode 100644
index 00000000000..e864a674cdd
--- /dev/null
+++ b/app/assets/javascripts/logo.js.coffee
@@ -0,0 +1,43 @@
+NProgress.configure(showSpinner: false)
+
+defaultClass = 'tanuki-shape'
+pieces = [
+ 'path#tanuki-right-cheek',
+ 'path#tanuki-right-eye, path#tanuki-right-ear',
+ 'path#tanuki-nose',
+ 'path#tanuki-left-eye, path#tanuki-left-ear',
+ 'path#tanuki-left-cheek',
+]
+pieceIndex = 0
+firstPiece = pieces[0]
+
+currentTimer = null
+delay = 150
+
+clearHighlights = ->
+ $(".#{defaultClass}.highlight").attr('class', defaultClass)
+
+start = ->
+ clearHighlights()
+ pieceIndex = 0
+ pieces.reverse() unless pieces[0] == firstPiece
+ currentTimer = setInterval(work, delay)
+
+stop = ->
+ clearInterval(currentTimer)
+ clearHighlights()
+
+work = ->
+ clearHighlights()
+ $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
+
+ # If we hit the last piece, reset the index and then reverse the array to
+ # get a nice back-and-forth sweeping look
+ if pieceIndex == pieces.length - 1
+ pieceIndex = 0
+ pieces.reverse()
+ else
+ pieceIndex++
+
+$(document).on('page:fetch', start)
+$(document).on('page:change', stop)
diff --git a/app/assets/javascripts/markdown_preview.js.coffee b/app/assets/javascripts/markdown_preview.js.coffee
new file mode 100644
index 00000000000..98fc8f17340
--- /dev/null
+++ b/app/assets/javascripts/markdown_preview.js.coffee
@@ -0,0 +1,87 @@
+# MarkdownPreview
+#
+# Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
+# and showing a warning when more than `x` users are referenced.
+#
+class @MarkdownPreview
+ # Minimum number of users referenced before triggering a warning
+ referenceThreshold: 10
+
+ showPreview: (form) ->
+ preview = form.find('.js-md-preview')
+ mdText = form.find('textarea.markdown-area').val()
+
+ if mdText.trim().length == 0
+ preview.text('Nothing to preview.')
+ @hideReferencedUsers(form)
+ else
+ preview.text('Loading...')
+ @renderMarkdown mdText, (response) =>
+ preview.html(response.body)
+ preview.syntaxHighlight()
+ @renderReferencedUsers(response.references.users, form)
+
+ renderMarkdown: (text, success) ->
+ return unless window.markdown_preview_path
+
+ $.ajax
+ type: 'POST'
+ url: window.markdown_preview_path
+ data: { text: text }
+ dataType: 'json'
+ success: success
+
+ hideReferencedUsers: (form) ->
+ referencedUsers = form.find('.referenced-users')
+ referencedUsers.hide()
+
+ renderReferencedUsers: (users, form) ->
+ referencedUsers = form.find('.referenced-users')
+
+ if referencedUsers.length
+ if users.length >= @referenceThreshold
+ referencedUsers.show()
+ referencedUsers.find('.js-referenced-users-count').text(users.length)
+ else
+ referencedUsers.hide()
+
+markdownPreview = new MarkdownPreview()
+
+previewButtonSelector = '.js-md-preview-button'
+writeButtonSelector = '.js-md-write-button'
+
+$.fn.setupMarkdownPreview = ->
+ $form = $(this)
+
+ form_textarea = $form.find('textarea.markdown-area')
+
+ form_textarea.on 'input', -> markdownPreview.hideReferencedUsers($form)
+ form_textarea.on 'blur', -> markdownPreview.showPreview($form)
+
+$(document).on 'click', previewButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ # toggle tabs
+ $form.find(writeButtonSelector).parent().removeClass('active')
+ $form.find(previewButtonSelector).parent().addClass('active')
+
+ # toggle content
+ $form.find('.md-write-holder').hide()
+ $form.find('.md-preview-holder').show()
+
+ markdownPreview.showPreview($form)
+
+$(document).on 'click', writeButtonSelector, (e) ->
+ e.preventDefault()
+
+ $form = $(this).closest('form')
+
+ # toggle tabs
+ $form.find(writeButtonSelector).parent().addClass('active')
+ $form.find(previewButtonSelector).parent().removeClass('active')
+
+ # toggle content
+ $form.find('.md-write-holder').show()
+ $form.find('.md-preview-holder').hide()
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index b21cb7904b5..9047587db81 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -40,12 +40,12 @@ class @MergeRequest
this.$('.all-commits').removeClass 'hide'
initTaskList: ->
- $('.merge-request-details .js-task-list-container').taskList('enable')
- $(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList
+ $('.detail-page-description .js-task-list-container').taskList('enable')
+ $(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
disableTaskList: ->
- $('.merge-request-details .js-task-list-container').taskList('disable')
- $(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container'
+ $('.detail-page-description .js-task-list-container').taskList('disable')
+ $(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
# TODO (rspeicher): Make the merge request description inline-editable like a
# note so that we can re-use its form here
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 593a8f42130..9e2dc1250c9 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -43,6 +43,7 @@
#
class @MergeRequestTabs
diffsLoaded: false
+ buildsLoaded: false
commitsLoaded: false
constructor: (@opts = {}) ->
@@ -54,6 +55,12 @@ class @MergeRequestTabs
bindEvents: ->
$(document).on 'shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', @tabShown
+ $(document).on 'click', '.js-show-tab', @showTab
+
+ showTab: (event) =>
+ event.preventDefault()
+
+ @activateTab $(event.target).data('action')
tabShown: (event) =>
$target = $(event.target)
@@ -63,12 +70,14 @@ class @MergeRequestTabs
@loadCommits($target.attr('href'))
else if action == 'diffs'
@loadDiff($target.attr('href'))
+ else if action == 'builds'
+ @loadBuilds($target.attr('href'))
@setCurrentAction(action)
scrollToElement: (container) ->
if window.location.hash
- $el = $("#{container} #{window.location.hash}")
+ $el = $("div#{container} #{window.location.hash}")
$('body').scrollTo($el.offset().top) if $el.length
# Activate a tab based on the current action
@@ -101,7 +110,7 @@ class @MergeRequestTabs
action = 'notes' if action == 'show'
# Remove a trailing '/commits' or '/diffs'
- new_state = @_location.pathname.replace(/\/(commits|diffs)(\.html)?\/?$/, '')
+ new_state = @_location.pathname.replace(/\/(commits|diffs|builds)(\.html)?\/?$/, '')
# Append the new action if we're on a tab other than 'notes'
unless action == 'notes'
@@ -124,7 +133,7 @@ class @MergeRequestTabs
@_get
url: "#{source}.json"
success: (data) =>
- document.getElementById('commits').innerHTML = data.html
+ document.querySelector("div#commits").innerHTML = data.html
$('.js-timeago').timeago()
@commitsLoaded = true
@scrollToElement("#commits")
@@ -135,10 +144,22 @@ class @MergeRequestTabs
@_get
url: "#{source}.json" + @_location.search
success: (data) =>
- document.getElementById('diffs').innerHTML = data.html
+ document.querySelector("div#diffs").innerHTML = data.html
+ $('div#diffs .js-syntax-highlight').syntaxHighlight()
@diffsLoaded = true
@scrollToElement("#diffs")
+ loadBuilds: (source) ->
+ return if @buildsLoaded
+
+ @_get
+ url: "#{source}.json"
+ success: (data) =>
+ document.querySelector("div#builds").innerHTML = data.html
+ $('.js-timeago').timeago()
+ @buildsLoaded = true
+ @scrollToElement("#builds")
+
# Show or hide the loading spinner
#
# status - Boolean, true to show, false to hide
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index 3176e5a8965..738ffc8343b 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -10,17 +10,20 @@ class @MergeRequestWidget
constructor: (@opts) ->
modal = $('#modal_merge_info').modal(show: false)
- mergeInProgress: ->
+ mergeInProgress: (deleteSourceBranch = false)->
$.ajax
type: 'GET'
url: $('.merge-request').data('url')
success: (data) =>
if data.state == "merged"
- location.reload()
+ urlSuffix = if deleteSourceBranch then '?delete_source=true' else ''
+
+ window.location.href = window.location.pathname + urlSuffix
else if data.merge_error
$('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
else
- setTimeout(merge_request_widget.mergeInProgress, 2000)
+ callback = -> merge_request_widget.mergeInProgress(deleteSourceBranch)
+ setTimeout(callback, 2000)
dataType: 'json'
getMergeStatus: ->
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
index 83434c1b9ba..b3c73ffce5d 100644
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ b/app/assets/javascripts/merge_requests.js.coffee
@@ -16,7 +16,7 @@
form = $("#issue_search_form")
search = $("#issue_search").val()
$('.merge-requests-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '? '+ form.serialize()
+ issues_url = form.attr('action') + '?' + form.serialize()
$.ajax
type: "GET"
@@ -27,7 +27,7 @@
success: (data) ->
$('.merge-requests-holder').html(data.html)
# Change url so if user reload a page - search results are saved
- History.replaceState {page: issues_url}, document.title, issues_url
+ history.replaceState {page: issues_url}, document.title, issues_url
MergeRequests.reload()
dataType: "json"
diff --git a/app/assets/javascripts/new_branch_form.js.coffee b/app/assets/javascripts/new_branch_form.js.coffee
new file mode 100644
index 00000000000..4b350854f78
--- /dev/null
+++ b/app/assets/javascripts/new_branch_form.js.coffee
@@ -0,0 +1,78 @@
+class @NewBranchForm
+ constructor: (form, availableRefs) ->
+ @branchNameError = form.find('.js-branch-name-error')
+ @name = form.find('.js-branch-name')
+ @ref = form.find('#ref')
+
+ @setupAvailableRefs(availableRefs)
+ @setupRestrictions()
+ @addBinding()
+ @init()
+
+ addBinding: ->
+ @name.on 'blur', @validate
+
+ init: ->
+ @name.trigger 'blur' if @name.val().length > 0
+
+ setupAvailableRefs: (availableRefs) ->
+ @ref.autocomplete
+ source: availableRefs,
+ minLength: 1
+
+ setupRestrictions: ->
+ startsWith = {
+ pattern: /^(\/|\.)/g,
+ prefix: "can't start with",
+ conjunction: "or"
+ }
+
+ endsWith = {
+ pattern: /(\/|\.|\.lock)$/g,
+ prefix: "can't end in",
+ conjunction: "or"
+ }
+
+ invalid = {
+ pattern: /(\s|~|\^|:|\?|\*|\[|\\|\.\.|@\{|\/{2,}){1}/g
+ prefix: "can't contain",
+ conjunction: ", "
+ }
+
+ single = {
+ pattern: /^@+$/g
+ prefix: "can't be",
+ conjunction: "or"
+ }
+
+ @restrictions = [startsWith, invalid, endsWith, single]
+
+ validate: =>
+ @branchNameError.empty()
+
+ unique = (values, value) ->
+ values.push(value) unless value in values
+ values
+
+ formatter = (values, restriction) ->
+ formatted = values.map (value) ->
+ switch
+ when /\s/.test value then 'spaces'
+ when /\/{2,}/g.test value then 'consecutive slashes'
+ else "'#{value}'"
+
+ "#{restriction.prefix} #{formatted.join(restriction.conjunction)}"
+
+ validator = (errors, restriction) =>
+ matched = @name.val().match(restriction.pattern)
+
+ if matched
+ errors.concat formatter(matched.reduce(unique, []), restriction)
+ else
+ errors
+
+ errors = @restrictions.reduce validator, []
+
+ if errors.length > 0
+ errorMessage = $("<span/>").text(errors.join(', '))
+ @branchNameError.append(errorMessage)
diff --git a/app/assets/javascripts/new_commit_form.js.coffee b/app/assets/javascripts/new_commit_form.js.coffee
new file mode 100644
index 00000000000..03f0f51acfa
--- /dev/null
+++ b/app/assets/javascripts/new_commit_form.js.coffee
@@ -0,0 +1,21 @@
+class @NewCommitForm
+ constructor: (form) ->
+ @newBranch = form.find('.js-target-branch')
+ @originalBranch = form.find('.js-original-branch')
+ @createMergeRequest = form.find('.js-create-merge-request')
+ @createMergeRequestContainer = form.find('.js-create-merge-request-container')
+
+ @renderDestination()
+ @newBranch.keyup @renderDestination
+
+ renderDestination: =>
+ different = @newBranch.val() != @originalBranch.val()
+
+ if different
+ @createMergeRequestContainer.show()
+ @createMergeRequest.prop('checked', true) unless @wasDifferent
+ else
+ @createMergeRequestContainer.hide()
+ @createMergeRequest.prop('checked', false)
+
+ @wasDifferent = different
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index ea75c656bcc..9e5204bfeeb 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -111,15 +111,25 @@ class @Notes
Note: for rendering inline notes use renderDiscussionNote
###
renderNote: (note) ->
+ unless note.valid
+ if note.award
+ flash = new Flash('You have already used this award emoji!', 'alert')
+ flash.pinTo('.header-content')
+ return
+
# render note if it not present in loaded list
# or skip if rendered
- if @isNewNote(note)
+ if @isNewNote(note) && !note.award
@note_ids.push(note.id)
$('ul.main-notes-list').
append(note.html).
syntaxHighlight()
@initTaskList()
+ if note.award
+ awards_handler.addAwardToEmojiBar(note.note)
+ awards_handler.scrollToAwards()
+
###
Check if note does not exists on page
###
@@ -138,6 +148,8 @@ class @Notes
@note_ids.push(note.id)
form = $("form[rel='" + note.discussion_id + "']")
row = form.closest("tr")
+ note_html = $(note.html)
+ note_html.syntaxHighlight()
# is this the first note of discussion?
if row.is(".js-temp-notes-holder")
@@ -148,14 +160,16 @@ class @Notes
row.next().find(".note").remove()
# Add note to 'Changes' page discussions
- $(".notes[rel='" + note.discussion_id + "']").append note.html
+ $(".notes[rel='" + note.discussion_id + "']").append note_html
# Init discussion on 'Discussion' page if it is merge request page
if $('body').attr('data-page').indexOf('projects:merge_request') == 0
- $('ul.main-notes-list').append(note.discussion_with_diff_html)
+ discussion_html = $(note.discussion_with_diff_html)
+ discussion_html.syntaxHighlight()
+ $('ul.main-notes-list').append(discussion_html)
else
# append new note to all matching discussions
- $(".notes[rel='" + note.discussion_id + "']").append note.html
+ $(".notes[rel='" + note.discussion_id + "']").append note_html
# cleanup after successfully creating a diff/discussion note
@removeDiscussionNoteForm(form)
@@ -255,7 +269,6 @@ class @Notes
###
addNote: (xhr, note, status) =>
@renderNote(note)
- @updateVotes()
###
Called in response to the new note form being submitted
@@ -277,7 +290,7 @@ class @Notes
$html.find('.js-task-list-container').taskList('enable')
# Find the note's `li` element by ID and replace it with the updated HTML
- $note_li = $("#note_#{note.id}")
+ $note_li = $('.note-row-' + note.id)
$note_li.replaceWith($html)
###
@@ -337,18 +350,26 @@ class @Notes
###
removeNote: ->
note = $(this).closest(".note")
- notes = note.closest(".notes")
+ note_id = note.attr('id')
- # check if this is the last note for this line
- if notes.find(".note").length is 1
+ $('.note[id="' + note_id + '"]').each ->
+ note = $(this)
+ notes = note.closest(".notes")
+ count = notes.closest(".notes_holder").find(".discussion-notes-count")
- # for discussions
- notes.closest(".discussion").remove()
+ # check if this is the last note for this line
+ if notes.find(".note").length is 1
- # for diff lines
- notes.closest("tr").remove()
+ # for discussions
+ notes.closest(".discussion").remove()
+
+ # for diff lines
+ notes.closest("tr").remove()
+ else
+ # update notes count
+ count.get(0).lastChild.nodeValue = " #{notes.children().length - 1}"
- note.remove()
+ note.remove()
###
Called in response to clicking the delete attachment link
@@ -360,8 +381,8 @@ class @Notes
note = $(this).closest(".note")
note.find(".note-attachment").remove()
note.find(".note-body > .note-text").show()
- note.find(".js-note-attachment-delete").hide()
- note.find(".note-edit-form").hide()
+ note.find(".note-header").show()
+ note.find(".current-note-edit-form").remove()
###
Called when clicking on the "reply" button for a diff line.
@@ -473,9 +494,6 @@ class @Notes
form = $(e.target).closest(".js-discussion-note-form")
@removeDiscussionNoteForm(form)
- updateVotes: ->
- true
-
###
Called after an attachment file has been selected.
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 0ea8fffce07..d7a658f8faa 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -1,13 +1,23 @@
class @Project
constructor: ->
- # Git clone panel switcher
- cloneHolder = $('.git-clone-holder')
- if cloneHolder.length
- $('a, button', cloneHolder).click ->
- $('a, button', cloneHolder).removeClass 'active'
- $(@).addClass 'active'
- $('#project_clone', cloneHolder).val $(@).data 'clone'
- $(".clone").text("").append $(@).data 'clone'
+ # Git protocol switcher
+ $('ul.clone-options-dropdown a').click ->
+ return if $(@).hasClass('active')
+
+
+ # Remove the active class for all buttons (ssh, http, kerberos if shown)
+ $('.active').not($(@)).removeClass('active');
+ # Add the active class for the clicked button
+ $(@).toggleClass('active')
+
+ url = $("#project_clone").val()
+ console.log("url",url)
+
+ # Update the input field
+ $('#project_clone').val(url)
+
+ # Update the command line instructions
+ $('.clone').text(url)
# Ref switcher
$('.project-refs-select').on 'change', ->
@@ -39,4 +49,4 @@ class @Project
when 4 then label = ' On Mention '
$('#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' \ No newline at end of file
+ $(@).parent().addClass 'active'
diff --git a/app/assets/javascripts/project_select.js.coffee b/app/assets/javascripts/project_select.js.coffee
new file mode 100644
index 00000000000..0ae274f3363
--- /dev/null
+++ b/app/assets/javascripts/project_select.js.coffee
@@ -0,0 +1,39 @@
+class @ProjectSelect
+ constructor: ->
+ $('.ajax-project-select').each (i, select) ->
+ @groupId = $(select).data('group-id')
+ @includeGroups = $(select).data('include-groups')
+
+ placeholder = "Search for project"
+ placeholder += " or group" if @includeGroups
+
+ $(select).select2
+ placeholder: placeholder
+ minimumInputLength: 0
+ query: (query) =>
+ finalCallback = (projects) ->
+ data = { results: projects }
+ query.callback(data)
+
+ if @includeGroups
+ projectsCallback = (projects) ->
+ groupsCallback = (groups) ->
+ data = groups.concat(projects)
+ finalCallback(data)
+
+ Api.groups query.term, false, groupsCallback
+ else
+ projectsCallback = finalCallback
+
+ if @groupId
+ Api.groupProjects @groupId, query.term, projectsCallback
+ else
+ Api.projects query.term, projectsCallback
+
+ id: (project) ->
+ project.web_url
+
+ text: (project) ->
+ project.name_with_namespace || project.name
+
+ dropdownCssClass: "ajax-project-dropdown"
diff --git a/app/assets/javascripts/projects_list.js.coffee b/app/assets/javascripts/projects_list.js.coffee
index db5faf71faf..f2887af190b 100644
--- a/app/assets/javascripts/projects_list.js.coffee
+++ b/app/assets/javascripts/projects_list.js.coffee
@@ -8,17 +8,17 @@ class @ProjectsList
$(".projects-list-filter").keyup ->
terms = $(this).val()
- uiBox = $(this).closest('.projects-list-holder')
+ uiBox = $('div.projects-list-holder')
if terms == "" || terms == undefined
- uiBox.find(".projects-list li").show()
+ uiBox.find("ul.projects-list li").show()
else
- uiBox.find(".projects-list li").each (index) ->
- name = $(this).find(".filter-title").text()
+ uiBox.find("ul.projects-list li").each (index) ->
+ name = $(this).find("span.filter-title").text()
if name.toLowerCase().search(terms.toLowerCase()) == -1
$(this).hide()
else
$(this).show()
- uiBox.find(".projects-list li.bottom").hide()
+ uiBox.find("ul.projects-list li.bottom").hide()
diff --git a/app/assets/javascripts/shortcuts.js.coffee b/app/assets/javascripts/shortcuts.js.coffee
index e9aeb1e9525..4d915bfc8c5 100644
--- a/app/assets/javascripts/shortcuts.js.coffee
+++ b/app/assets/javascripts/shortcuts.js.coffee
@@ -7,7 +7,7 @@ class @Shortcuts
selectiveHelp: (e) =>
Shortcuts.showHelp(e, @enabledHelp)
-
+
@showHelp: (e, location) ->
if $('#modal-shortcuts').length > 0
$('#modal-shortcuts').modal('show')
@@ -17,8 +17,7 @@ class @Shortcuts
dataType: 'script',
success: (e) ->
if location and location.length > 0
- for l in location
- $(l).show()
+ $(l).show() for l in location
else
$('.hidden-shortcut').show()
$('.js-more-help-button').remove()
@@ -28,3 +27,8 @@ class @Shortcuts
@focusSearch: (e) ->
$('#search').focus()
e.preventDefault()
+
+$(document).on 'click.more_help', '.js-more-help-button', (e) ->
+ $(@).remove()
+ $('.hidden-shortcut').show()
+ e.preventDefault()
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index fb08016fbae..ae59480af9e 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -5,6 +5,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
$('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
$('header').toggleClass("header-collapsed header-expanded")
+ $('.sidebar-wrapper').toggleClass("sidebar-collapsed sidebar-expanded")
$('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
$.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
)
diff --git a/app/assets/javascripts/star.js.coffee b/app/assets/javascripts/star.js.coffee
new file mode 100644
index 00000000000..d849b2e7950
--- /dev/null
+++ b/app/assets/javascripts/star.js.coffee
@@ -0,0 +1,22 @@
+class @Star
+ constructor: ->
+ $('.project-home-panel .toggle-star').on('ajax:success', (e, data, status, xhr) ->
+ $this = $(this)
+ $starSpan = $this.find('span')
+ $starIcon = $this.find('i')
+
+ toggleStar = (isStarred) ->
+ $this.parent().find('span.count').text data.star_count
+ if isStarred
+ $starSpan.removeClass('starred').text 'Star'
+ $starIcon.removeClass('fa-star').addClass 'fa-star-o'
+ else
+ $starSpan.addClass('starred').text 'Unstar'
+ $starIcon.removeClass('fa-star-o').addClass 'fa-star'
+ return
+
+ toggleStar $starSpan.hasClass('starred')
+ return
+ ).on 'ajax:error', (e, xhr, status, error) ->
+ new Flash('Star toggle failed. Try again later.', 'alert')
+ return \ No newline at end of file
diff --git a/app/assets/javascripts/stat_graph_contributors_util.js.coffee b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
index cfe5508290f..f5584bcfe4b 100644
--- a/app/assets/javascripts/stat_graph_contributors_util.js.coffee
+++ b/app/assets/javascripts/stat_graph_contributors_util.js.coffee
@@ -6,7 +6,7 @@ window.ContributorsStatGraphUtil =
for entry in log
@add_date(entry.date, total) unless total[entry.date]?
- data = by_author[entry.author_name] #|| by_email[entry.author_email]
+ data = by_author[entry.author_name] || by_email[entry.author_email]
data ?= @add_author(entry, by_author, by_email)
@add_date(entry.date, data) unless data[entry.date]
@@ -95,5 +95,4 @@ window.ContributorsStatGraphUtil =
if date_range is null || date_range[0] <= new Date(date) <= date_range[1]
true
else
- false
-
+ false \ No newline at end of file
diff --git a/app/assets/javascripts/user.js.coffee b/app/assets/javascripts/user.js.coffee
index d0d81f96921..ec4271b092c 100644
--- a/app/assets/javascripts/user.js.coffee
+++ b/app/assets/javascripts/user.js.coffee
@@ -2,3 +2,9 @@ class @User
constructor: ->
$('.profile-groups-avatars').tooltip("placement": "top")
new ProjectsList()
+
+ $('.hide-project-limit-message').on 'click', (e) ->
+ path = '/'
+ $.cookie('hide_project_limit_message', 'false', { path: path })
+ $(@).parents('.project-limit-message').remove()
+ e.preventDefault()
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9157562a5c5..9467011799f 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -32,17 +32,15 @@ class @UsersSelect
if showNullUser
nullUser = {
name: 'Unassigned',
- avatar: null,
- username: 'none',
id: 0
}
data.results.unshift(nullUser)
if showAnyUser
+ name = showAnyUser
+ name = 'Any User' if name == true
anyUser = {
- name: 'Any',
- avatar: null,
- username: 'none',
+ name: name,
id: null
}
data.results.unshift(anyUser)
@@ -50,7 +48,6 @@ class @UsersSelect
if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
emailUser = {
name: "Invite \"#{query.term}\"",
- avatar: null,
username: query.term,
id: query.term
}
@@ -58,11 +55,8 @@ class @UsersSelect
query.callback(data)
- initSelection: (element, callback) =>
- id = $(element).val()
- if id != "" && id != "0"
- @user(id, callback)
-
+ initSelection: (args...) =>
+ @initSelection(args...)
formatResult: (args...) =>
@formatResult(args...)
formatSelection: (args...) =>
@@ -71,16 +65,24 @@ class @UsersSelect
escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
m
+ initSelection: (element, callback) ->
+ id = $(element).val()
+ if id == "0"
+ nullUser = { name: 'Unassigned' }
+ callback(nullUser)
+ else if id != ""
+ @user(id, callback)
+
formatResult: (user) ->
if user.avatar_url
avatar = user.avatar_url
else
avatar = gon.default_avatar_url
- "<div class='user-result'>
+ "<div class='user-result #{'no-username' unless user.username}'>
<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>
<div class='user-name'>#{user.name}</div>
- <div class='user-username'>#{user.username}</div>
+ <div class='user-username'>#{user.username || ""}</div>
</div>"
formatSelection: (user) ->
@@ -115,5 +117,5 @@ class @UsersSelect
callback(users)
buildUrl: (url) ->
- url = gon.relative_url_root + url if gon.relative_url_root?
+ url = gon.relative_url_root.replace(/\/$/, '') + url if gon.relative_url_root?
return url
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 7b060ce4853..0c0451fe4dd 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -2,8 +2,8 @@
* This is a manifest file that'll automatically include all the stylesheets available in this directory
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
* the top of the compiled file, but it's generally better to create a new file per style scope.
- *= require jquery.ui.datepicker
- *= require jquery.ui.autocomplete
+ *= require jquery-ui/datepicker
+ *= require jquery-ui/autocomplete
*= require jquery.atwho
*= require select2
*= require_self
@@ -48,4 +48,4 @@
/*
* Styles for JS behaviors.
*/
-@import "behaviors.scss"; \ No newline at end of file
+@import "behaviors.scss";
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 1ec9d2fd84f..48a4971c8fc 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -1,9 +1,9 @@
@import "framework/fonts";
@import "framework/variables";
@import "framework/mixins";
-@import "framework/layout";
@import 'framework/tw_bootstrap_variables';
@import 'framework/tw_bootstrap';
+@import "framework/layout";
@import "framework/avatar.scss";
@import "framework/blocks.scss";
@@ -25,6 +25,7 @@
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
@import "framework/pagination.scss";
+@import "framework/panels.scss";
@import "framework/selects.scss";
@import "framework/sidebar.scss";
@import "framework/tables.scss";
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 5949a0fd5ad..206d39cc9b3 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -28,6 +28,10 @@
border-bottom: 1px solid $border-color;
color: $gl-gray;
+ &.oneline-block {
+ line-height: 42px;
+ }
+
&.white {
background-color: white;
}
@@ -64,11 +68,15 @@
.oneline {
line-height: 42px;
}
+
+ > p:last-child {
+ margin-bottom: 0;
+ }
}
.cover-block {
text-align: center;
- background: #f7f8fa;
+ background: $background-color;
margin: -$gl-padding;
margin-bottom: 0;
padding: 44px $gl-padding;
@@ -100,7 +108,7 @@
}
.cover-desc {
- padding: 0 $gl-padding;
+ padding: 0 $gl-padding 3px;
color: $gl-text-color;
}
@@ -108,5 +116,14 @@
position: absolute;
top: 10px;
right: 10px;
+
+ &.left {
+ left: 10px;
+ right: auto;
+ }
}
}
+
+.block-connector {
+ margin-top: -1px;
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index e5f0c0ad9ef..97a94638847 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,10 +1,9 @@
@mixin btn-default {
- @include border-radius(2px);
+ @include border-radius(3px);
border-width: 1px;
border-style: solid;
- text-transform: uppercase;
- font-size: 13px;
- font-weight: 600;
+ font-size: 15px;
+ font-weight: 500;
line-height: 18px;
padding: 11px $gl-padding;
letter-spacing: .4px;
@@ -18,7 +17,7 @@
@mixin btn-middle {
@include btn-default;
- @include border-radius(2px);
+ @include border-radius(3px);
padding: 11px 24px;
}
@@ -51,6 +50,10 @@
@include btn-color($blue-light, $border-blue-light, $blue-normal, $border-blue-normal, $blue-dark, $border-blue-dark, #FFFFFF);
}
+@mixin btn-blue-medium {
+ @include btn-color($blue-medium-light, $border-blue-light, $blue-medium, $border-blue-normal, $blue-medium-dark, $border-blue-dark, #FFFFFF);
+}
+
@mixin btn-orange {
@include btn-color($orange-light, $border-orange-light, $orange-normal, $border-orange-normal, $orange-dark, $border-orange-dark, #FFFFFF);
}
@@ -60,7 +63,7 @@
}
@mixin btn-gray {
- @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, #313236);
+ @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-light, $gray-dark, $border-gray-dark, #313236);
}
@mixin btn-white {
@@ -75,6 +78,10 @@
padding: 5px 10px;
}
+ &.btn-nr {
+ padding: 7px 10px;
+ }
+
&.btn-xs {
padding: 1px 5px;
}
@@ -91,11 +98,15 @@
@include btn-gray;
}
- &.btn-primary,
+ &.btn-primary {
+ @include btn-blue-medium;
+ }
+
&.btn-info {
@include btn-blue;
}
+ &.btn-close,
&.btn-warning {
@include btn-orange;
}
@@ -110,20 +121,8 @@
float: right;
}
- &.btn-close {
- color: $gl-danger;
- border-color: $gl-danger;
- &:hover {
- color: #B94A48;
- }
- }
-
&.btn-reopen {
- color: $gl-success;
- border-color: $gl-success;
- &:hover {
- color: #468847;
- }
+ /* should be same as parent class for now */
}
&.btn-grouped {
@@ -162,10 +161,25 @@
border-color: #e7e9ed;
width: 140px;
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+
&.active {
border-color: $gl-info;
background: $gl-info;
color: #fff;
+
+ .badge {
+ color: $gl-info;
+ background-color: white;
+ }
}
}
}
+
+.btn-clipboard {
+ border: none;
+}
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index a36fefe22c5..580012abd77 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -19,38 +19,33 @@
}
}
}
+
/**
* This overwrites the default values of the cal-heatmap gem
*/
.calendar {
.qi {
- background-color: #999;
fill: #fff;
}
.q1 {
- background-color: #dae289;
- fill: #ededed;
+ fill: #ededed !important;
}
.q2 {
- background-color: #cedb9c;
- fill: #ACD5F2;
+ fill: #ACD5F2 !important;
}
.q3 {
- background-color: #b5cf6b;
- fill: #7FA8D1;
+ fill: #7FA8D1 !important;
}
.q4 {
- background-color: #637939;
- fill: #49729B;
+ fill: #49729B !important;
}
.q5 {
- background-color: #3b6427;
- fill: #254E77;
+ fill: #254E77 !important;
}
.domain-background {
@@ -59,32 +54,7 @@
}
.ch-tooltip {
- position: absolute;
- display: none;
- margin-top: 22px;
- margin-left: 1px;
- font-size: 13px;
padding: 3px;
font-weight: 550;
- background-color: #222;
- span {
- position: absolute;
- width: 200px;
- text-align: center;
- visibility: hidden;
- border-radius: 10px;
- &:after {
- content: '';
- position: absolute;
- top: 100%;
- left: 50%;
- margin-left: -8px;
- width: 0;
- height: 0;
- border-top: 8px solid #000000;
- border-right: 8px solid transparent;
- border-left: 8px solid transparent;
- }
- }
}
}
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index f1699d21c9b..20a9bfb9816 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -7,11 +7,11 @@
/* Common styles for all types */
.bs-callout {
- margin: 20px 0;
- padding: 20px;
- border-left: 3px solid #eee;
- color: #666;
- background: #f9f9f9;
+ margin: $gl-padding 0;
+ padding: $gl-padding;
+ border-left: 3px solid $border-color;
+ color: $text-color;
+ background: $background-color;
}
.bs-callout h4 {
margin-top: 0;
@@ -42,4 +42,3 @@
border-color: #5cA64d;
color: #3c763d;
}
-
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index e1a1793be9c..11730000f85 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -7,7 +7,7 @@
/** COMMON CLASSES **/
.prepend-top-10 { margin-top:10px }
-.prepend-top-default { margin-top: $gl-padding; }
+.prepend-top-default { margin-top: $gl-padding !important; }
.prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px }
.prepend-left-20 { margin-left:20px }
@@ -16,6 +16,7 @@
.append-bottom-10 { margin-bottom:10px }
.append-bottom-15 { margin-bottom:15px }
.append-bottom-20 { margin-bottom:20px }
+.append-bottom-default { margin-bottom: $gl-padding; }
.inline { display: inline-block }
.center { text-align: center }
@@ -51,6 +52,10 @@ pre {
}
}
+hr {
+ margin: $gl-padding 0;
+}
+
.dropdown-menu > li > a {
text-shadow: none;
}
@@ -63,7 +68,7 @@ pre {
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background: $gl-primary;
- color: #FFF
+ color: #FFF;
}
.str-truncated {
@@ -327,15 +332,15 @@ table {
}
}
+.well {
+ margin-bottom: $gl-padding;
+}
+
.search_box {
@extend .well;
text-align: center;
}
-.task-status {
- margin-left: 10px;
-}
-
#nprogress .spinner {
top: 15px !important;
right: 10px !important;
@@ -369,14 +374,13 @@ table {
}
}
-.center-top-menu {
+.center-top-menu, .left-top-menu {
@include nav-menu;
text-align: center;
margin-top: 5px;
margin-bottom: $gl-padding;
- height: 56px;
+ height: auto;
margin-top: -$gl-padding;
- padding-top: $gl-padding;
&.no-bottom {
margin-bottom: 0;
@@ -385,6 +389,58 @@ table {
&.no-top {
margin-top: 0;
}
+
+ li a {
+ display: inline-block;
+ padding-top: $gl-padding;
+ padding-bottom: 11px;
+ margin-bottom: -1px;
+ }
+
+ &.bottom-border {
+ border-bottom: 1px solid $border-color;
+ height: 57px;
+ }
+
+ &.wide {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ }
+}
+
+.left-top-menu {
+ text-align: left;
+ border-bottom: 1px solid #EEE;
+}
+
+.center-middle-menu {
+ @include nav-menu;
+ padding: 0;
+ text-align: center;
+ margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: 0;
+ height: 58px;
+ border-bottom: 1px solid $border-color;
+
+ li {
+ &:after {
+ content: "|";
+ color: $border-gray-light;
+ }
+
+ &:last-child {
+ &:after {
+ content: none;
+ }
+ }
+
+ > a {
+ display: inline-block;
+ text-transform: uppercase;
+ font-size: 13px;
+ }
+ }
}
.dropzone .dz-preview .dz-progress {
@@ -398,3 +454,26 @@ table {
.space-right {
margin-right: 10px;
}
+
+.alert, .progress {
+ margin-bottom: $gl-padding;
+}
+
+.new-project-item-select-holder {
+ display: inline-block;
+ position: relative;
+
+ .new-project-item-select {
+ position: absolute;
+ top: 0;
+ right: 0;
+ width: 250px !important;
+ visibility: hidden;
+ }
+}
+
+.content-separator {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ border-top: 1px solid $border-color;
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 35db00281e5..cbfd4bc29b6 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -8,7 +8,6 @@
border: none;
border-top: 1px solid #E7E9EE;
border-bottom: 1px solid #E7E9EE;
- margin-bottom: 1em;
&.readme-holder {
border-bottom: 0;
@@ -22,10 +21,9 @@
position: relative;
background: $background-color;
border-bottom: 1px solid $border-color;
- text-shadow: 0 1px 1px #fff;
margin: 0;
text-align: left;
- padding: 10px 15px;
+ padding: 10px $gl-padding;
.file-actions {
float: right;
@@ -171,4 +169,3 @@
}
}
}
-
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 0edfe24f195..032d343df44 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -22,9 +22,10 @@ input[type='text'].danger {
}
.form-actions {
- padding: 17px 20px 18px;
- margin-top: 18px;
- margin-bottom: 18px;
+ margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: -$gl-padding;
+ padding: $gl-padding;
background-color: $background-color;
border-top: 1px solid $border-color;
}
@@ -73,6 +74,8 @@ label {
.form-control {
@include box-shadow(none);
+ height: 42px;
+ padding: 8px $gl-padding;
}
.wiki-content {
@@ -88,7 +91,19 @@ label {
}
.input-group {
+ .select2-container {
+ display: table-cell;
+ width: 200px !important;
+ }
.input-group-addon {
background-color: #f7f8fa;
}
+ .input-group-addon:not(:first-child):not(:last-child) {
+ border-left: 0;
+ border-right: 0;
+ }
+}
+
+.help-block {
+ margin-bottom: 0;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 91e6975e269..4dbbb56104b 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -6,15 +6,17 @@ header {
transition-duration: .3s;
&.navbar-empty {
+ height: 58px;
background: #FFF;
border-bottom: 1px solid #EEE;
.center-logo {
- margin: 8px 0;
+ margin: 11px 0;
text-align: center;
- img {
- height: 32px;
+ #tanuki-logo, img {
+ width: 36px;
+ height: 36px;
}
}
}
@@ -118,6 +120,10 @@ header {
}
}
}
+
+ .impersonation i {
+ color: $red-normal;
+ }
}
@mixin collapsed-header {
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 93377e45e70..e93dbab0c42 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -4,31 +4,32 @@
*
*/
-.issue-box {
- @include border-radius(2px);
+.status-box {
+ @include border-radius(3px);
- display: inline-block;
- padding: 10px $gl-padding;
+ display: block;
+ float: left;
+ padding: 0 $gl-padding;
font-weight: normal;
margin-right: 10px;
font-size: $gl-font-size;
- &.issue-box-closed {
+ &.status-box-closed {
background-color: $gl-danger;
color: #FFF;
}
- &.issue-box-merged {
+ &.status-box-merged {
background-color: $gl-primary;
color: #FFF;
}
- &.issue-box-open {
- background-color: #019875;
+ &.status-box-open {
+ background-color: $green-light;
color: #FFF;
}
- &.issue-box-expired {
+ &.status-box-expired {
background: #cea61b;
color: #FFF;
}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index c7b3b60e769..a1a9990241d 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -2,10 +2,13 @@ html {
overflow-y: scroll;
&.touch .tooltip { display: none !important; }
+}
+
+body {
+ background-color: #F3F3F3 !important;
- body {
- padding-top: $header-height;
- text-rendering: geometricPrecision;
+ &.navless {
+ background-color: white !important;
}
}
@@ -19,7 +22,8 @@ html {
}
.navless-container {
- margin-top: 30px;
+ margin-top: $header-height;
+ padding-top: $gl-padding * 2;
}
.container-limited {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index f6942db5816..1c74e525a60 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -7,7 +7,7 @@
padding: 0;
list-style: none;
- li {
+ > li {
padding: 10px 15px;
min-height: 20px;
border-bottom: 1px solid #eee;
@@ -72,13 +72,6 @@
}
}
-ol, ul {
- &.styled {
- li {
- padding: 2px;
- }
- }
-}
/** light list with border-bottom between li **/
ul.bordered-list {
@@ -95,8 +88,14 @@ ul.bordered-list {
}
}
-li.task-list-item {
- list-style-type: none;
+ul.task-list {
+ li.task-list-item {
+ list-style-type: none;
+ }
+
+ ul:not(.task-list) {
+ padding-left: 1.3em;
+ }
}
ul.content-list {
@@ -117,7 +116,7 @@ ul.content-list {
}
.controls {
- padding-top: 4px;
+ padding-top: 1px;
float: right;
.btn {
@@ -127,3 +126,36 @@ ul.content-list {
}
}
+.panel > .content-list {
+ li {
+ margin: 0;
+ }
+}
+
+ul.controls {
+ padding-top: 1px;
+ float: right;
+ list-style: none;
+
+ .btn {
+ padding: 10px 14px;
+ }
+
+ > li {
+ float: left;
+ margin-right: 10px;
+
+ &:last-child {
+ margin-right: 0;
+ }
+
+ .author_link {
+ display: inline-block;
+
+ .avatar-inline {
+ margin-left: 0;
+ margin-right: 0;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index ed0333d2336..4a00a197d9a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -73,11 +73,8 @@
}
.referenced-users {
- padding: 10px 0;
- color: #999;
- margin-left: 10px;
- margin-top: 1px;
- margin-right: 130px;
+ color: #4c4e54;
+ padding-top: 10px;
}
.md-preview-holder {
@@ -90,7 +87,7 @@
.new_note,
.edit_note,
-.issuable-description,
+.detail-page-description,
.milestone-description,
.wiki-content,
.merge-request-form {
@@ -106,6 +103,7 @@
}
.markdown-area {
+ @include border-radius(0);
background: #FFF;
border: 1px solid #ddd;
min-height: 140px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 089e6958eeb..41fd890f14f 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -72,9 +72,10 @@
list-style: none;
> li {
+ @include clearfix;
+
padding: 10px 0;
border-bottom: 1px solid #EEE;
- overflow: hidden;
display: block;
margin: 0px;
@@ -122,7 +123,6 @@
padding: 0;
margin: 0;
list-style: none;
- margin-top: 5px;
height: 56px;
li {
@@ -130,31 +130,26 @@
a {
padding: 14px;
- font-size: 17px;
+ font-size: 15px;
line-height: 28px;
- color: #7f8fa4;
+ color: #959494;
border-bottom: 2px solid transparent;
&:hover, &:active, &:focus {
text-decoration: none;
+ outline: none;
}
}
&.active a {
- color: #4c4e54;
- border-bottom: 2px solid #1cacfc;
+ color: #616060;
+ border-bottom: 2px solid #4688f1;
}
.badge {
font-weight: normal;
- background-color: #fff;
background-color: #eee;
color: #78a;
}
}
}
-
-.fa-align {
- top: 20px;
- position: relative;
-}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index cea47fba192..c00709fb6bb 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -81,10 +81,7 @@
display: none;
}
- .center-top-menu {
- height: 45px;
- margin-bottom: 30px;
-
+ .center-top-menu, .left-top-menu {
li a {
font-size: 14px;
padding: 19px 10px;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index 6677f94dafd..2cd30491bf5 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -32,3 +32,7 @@
}
}
}
+
+.panel > .gl-pagination {
+ margin: 0;
+}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
new file mode 100644
index 00000000000..57b9451b264
--- /dev/null
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -0,0 +1,20 @@
+.panel {
+ margin-bottom: $gl-padding;
+
+ .panel-heading {
+ padding: 7px $gl-padding;
+ }
+
+ .panel-body {
+ padding: $gl-padding;
+
+ .form-actions {
+ margin: -$gl-padding;
+ margin-top: $gl-padding;
+ }
+ }
+}
+
+.container-blank .panel .panel-heading {
+ line-height: 42px !important;
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 78fff58d232..af145191bc8 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -15,6 +15,16 @@
border-left: none;
padding-top: 5px;
}
+
+ .select2-chosen {
+ color: $gl-text-color;
+ }
+
+ &.select2-default {
+ .select2-chosen {
+ color: #999;
+ }
+ }
}
}
@@ -23,6 +33,7 @@
border: 1px solid #e7e9ed;
}
+
.select2-drop {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px);
@@ -48,17 +59,38 @@
color: #313236;
}
+.select2-container-multi {
+ .select2-choices {
+ @include border-radius(2px);
+ border-color: $input-border;
+ background: white;
+ padding-left: $gl-padding / 2;
+
+ .select2-search-field input {
+ padding: $gl-padding / 2;
+ font-size: 13px;
+ height: auto;
+ font-family: inherit;
+ font-size: inherit;
+ }
-.select2-container-multi .select2-choices {
- @include border-radius(2px);
- border-color: #CCC;
-}
-
-.select2-container-multi .select2-choices .select2-search-field input {
- padding: 8px 14px;
- font-size: 13px;
- line-height: 18px;
- height: auto;
+ .select2-search-choice {
+ margin: 8px 0 0 8px;
+ background: white;
+ box-shadow: none;
+ border-color: $input-border;
+ color: $gl-text-color;
+ line-height: 15px;
+
+ .select2-search-choice-close {
+ top: 5px;
+ }
+
+ &.select2-search-choice-focus {
+ border-color: $gl-text-color;
+ }
+ }
+ }
}
.select2-drop-active {
@@ -123,10 +155,16 @@
}
.user-result {
+ min-height: 24px;
+
.user-image {
float: left;
}
- .user-name {
+
+ &.no-username {
+ .user-name {
+ line-height: 24px;
+ }
}
}
@@ -143,4 +181,4 @@
.ajax-users-dropdown {
min-width: 250px !important;
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 985ea164576..83243dd2457 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,4 +1,7 @@
.page-with-sidebar {
+ padding-top: $header-height;
+ transition-duration: .3s;
+
.sidebar-wrapper {
position: fixed;
top: 0;
@@ -14,19 +17,15 @@
.sidebar-wrapper {
z-index: 99;
background: $background-color;
- transition-duration: .3s;
}
.content-wrapper {
- min-height: 100vh;
width: 100%;
padding: 20px;
- background: #EAEBEC;
.container-fluid {
background: #FFF;
padding: $gl-padding;
- min-height: 90vh;
&.container-blank {
background: none;
@@ -36,6 +35,83 @@
}
}
+.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: 11px 0 11px 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: 14px;
+ font-size: 19px;
+ line-height: 41px;
+ font-weight: normal;
+ }
+ }
+ }
+
+ &:hover {
+ background-color: #EEE;
+ }
+ }
+
+ .sidebar-user {
+ padding: 9px 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;
+
+ &:hover, &.highlight {
+ fill: rgb(255, 255, 255);
+ transition: all 0.1s;
+ }
+}
+
+
.nav-sidebar {
margin-top: 14 + $header-height;
margin-bottom: 100px;
@@ -62,8 +138,9 @@
color: $gray;
display: block;
text-decoration: none;
- padding-left: 22px;
+ padding-left: 23px;
font-weight: normal;
+ outline: none;
&:hover {
text-decoration: none;
@@ -85,6 +162,10 @@
padding: 0px 8px;
@include border-radius(6px);
}
+
+ &.back-link i {
+ transition-duration: .3s;
+ }
}
}
}
@@ -100,7 +181,6 @@
@mixin expanded-sidebar {
padding-left: $sidebar_width;
- transition-duration: .3s;
.sidebar-wrapper {
width: $sidebar_width;
@@ -114,16 +194,15 @@
&.back-link {
i {
- visibility: hidden;
+ opacity: 0;
}
}
}
}
}
-@mixin folded-sidebar {
- padding-left: 60px;
- transition-duration: .3s;
+@mixin collapsed-sidebar {
+ padding-left: $sidebar_collapsed_width;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
@@ -132,7 +211,7 @@
width: $sidebar_collapsed_width;
a {
- padding-left: 12px;
+ padding-left: ($sidebar_collapsed_width - 36) / 2;
.gitlab-text-container {
display: none;
@@ -143,9 +222,13 @@
.nav-sidebar {
width: $sidebar_collapsed_width;
- li a {
- span {
- display: none;
+ li {
+ width: auto;
+
+ a {
+ span {
+ display: none;
+ }
}
}
}
@@ -155,7 +238,7 @@
}
.sidebar-user {
- padding-left: 12px;
+ padding-left: ($sidebar_collapsed_width - 36) / 2;
width: $sidebar_collapsed_width;
.username {
@@ -176,6 +259,7 @@
text-align: center;
line-height: 40px;
transition-duration: .3s;
+ outline: none;
}
.collapse-nav a:hover {
@@ -185,11 +269,11 @@
@media (max-width: $screen-md-max) {
.page-sidebar-collapsed {
- @include folded-sidebar;
+ @include collapsed-sidebar;
}
.page-sidebar-expanded {
- @include folded-sidebar;
+ @include collapsed-sidebar;
}
.collapse-nav {
@@ -199,82 +283,10 @@
@media(min-width: $screen-md-max) {
.page-sidebar-collapsed {
- @include folded-sidebar;
+ @include collapsed-sidebar;
}
.page-sidebar-expanded {
@include expanded-sidebar;
}
}
-
-.sidebar-user {
- padding: 9px 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;
- }
-}
-
-.sidebar-wrapper {
- .header-logo {
- border-bottom: 1px solid transparent;
- float: left;
- height: $header-height;
- width: $sidebar_width;
- overflow: hidden;
- transition-duration: .3s;
-
- a {
- float: left;
- height: $header-height;
- width: 100%;
- padding: 10px 22px;
- overflow: hidden;
-
- img {
- width: 36px;
- height: 36px;
- }
-
- #tanuki-logo, img {
- float: left;
- }
-
- .gitlab-text-container {
- width: 230px;
-
- h3 {
- width: 158px;
- float: left;
- margin: 0;
- margin-left: 14px;
- font-size: 19px;
- line-height: 41px;
- font-weight: normal;
- }
- }
- }
-
- &:hover {
- background-color: #EEE;
- }
- }
-}
-
-
-.tanuki-shape {
- transition: all 0.8s;
-
- &:hover {
- fill: rgb(255, 255, 255);
- transition: all 0.1s;
- }
-}
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 66e16e8df75..793ab3d9bb9 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -6,6 +6,8 @@
table {
&.table {
+ margin-bottom: $gl-padding;
+
.dropdown-menu a {
text-decoration: none;
}
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index eb53c4153d3..ff41e26ed8a 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -10,8 +10,7 @@
margin-left: -$gl-padding;
margin-right: -$gl-padding;
color: $gl-gray;
- border-bottom: 1px solid #ECEEF1;
- border-right: 1px solid #ECEEF1;
+ border-bottom: 1px solid $border-white-light;
&:target {
background: $hover;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 99d028d1228..94f0ed761df 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -172,7 +172,7 @@
}
.panel-body {
- form {
+ form, pre {
margin: 0;
}
@@ -190,6 +190,10 @@
.btn {
min-width: 124px;
}
+
+ .btn-clipboard {
+ min-width: 0px;
+ }
}
&.panel-small {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index e6558a23858..714369d9f15 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -54,17 +54,17 @@
h3 {
margin: 24px 0 12px 0;
- font-size: 1.25em;
+ font-size: 1.1em;
}
h4 {
margin: 24px 0 12px 0;
- font-size: 1.1em;
+ font-size: 0.98em;
}
h5 {
margin: 24px 0 12px 0;
- font-size: 1em;
+ font-size: 0.95em;
}
h6 {
@@ -173,7 +173,6 @@
*
*/
body {
- text-rendering:optimizeLegibility;
-webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px;
}
@@ -182,6 +181,10 @@ body {
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
+
+ &:last-child {
+ margin-bottom: 0;
+ }
}
.page-title-empty {
@@ -217,6 +220,7 @@ pre {
.monospace {
font-family: $monospace_font;
+ font-size: 90%;
}
code {
@@ -257,3 +261,9 @@ textarea.js-gfm-input {
.strikethrough {
text-decoration: line-through;
}
+
+h1, h2, h3, h4 {
+ small {
+ color: $gl-gray;
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 91954683c3e..af75123b0af 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,9 +1,9 @@
-$hover: #FFFAF1;
+$hover: #faf9f9;
$gl-text-color: #54565B;
$gl-text-green: #4A2;
$gl-text-red: #D12F19;
$gl-text-orange: #D90;
-$gl-header-color: #4c4e54;
+$gl-header-color: #323232;
$gl-link-color: #333c48;
$md-text-color: #444;
$md-link-color: #3084bb;
@@ -15,13 +15,14 @@ $sidebar_width: 230px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
-$border-color: #dce0e6;
+$border-color: #efeff1;
$table-border-color: #eef0f2;
-$background-color: #F7F8FA;
+$background-color: #faf9f9;
$header-height: 58px;
-$fixed-layout-width: 1200px;
-$gl-gray: #7f8fa4;
+$fixed-layout-width: 1280px;
+$gl-gray: #5a5a5a;
$gl-padding: 16px;
+$gl-padding-top:10px;
$gl-avatar-size: 46px;
/*
@@ -29,12 +30,12 @@ $gl-avatar-size: 46px;
*/
$white-light: #FFFFFF;
-$white-normal: #DCE0E5;
-$white-dark: #E4E7ED;
+$white-normal: #ededed;
+$white-dark: #ededed;
-$gray-light: #F0F2F5;
-$gray-normal: #DCE0E5;
-$gray-dark: #E4E7ED;
+$gray-light: #f7f7f7;
+$gray-normal: #ededed;
+$gray-dark: #ededed;
$green-light: #31AF64;
$green-normal: #2FAA60;
@@ -44,6 +45,10 @@ $blue-light: #2EA8E5;
$blue-normal: #2D9FD8;
$blue-dark: #2897CE;
+$blue-medium-light: #3498CB;
+$blue-medium: #2F8EBF;
+$blue-medium-dark: #2D86B4;
+
$orange-light: #FC6443;
$orange-normal: #E75E40;
$orange-dark: #CE5237;
@@ -52,11 +57,11 @@ $red-light: #F43263;
$red-normal: #E52C5A;
$red-dark: #D22852;
-$border-white-light: #E3E7EC;
+$border-white-light: #F1F2F4;
$border-white-normal: #D6DAE2;
$border-white-dark: #C6CACF;
-$border-gray-light: #DCE0E5;
+$border-gray-light: #d1d1d1;
$border-gray-normal: #D6DAE2;
$border-gray-dark: #C6CACF;
@@ -76,6 +81,8 @@ $border-red-light: #E52C5A;
$border-red-normal: #D22852;
$border-red-dark: #CA264F;
+/* header */
+$light-grey-header: #faf9f9;
/*
* State colors:
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
new file mode 100644
index 00000000000..87dd30f4111
--- /dev/null
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -0,0 +1,125 @@
+.awards {
+ @include clearfix;
+ line-height: 34px;
+
+ .emoji-icon {
+ width: 20px;
+ height: 20px;
+ margin: 7px 0 0 5px;
+ }
+
+ .award {
+ @include border-radius(5px);
+
+ border: 1px solid;
+ padding: 0px 10px;
+ float: left;
+ margin-right: 5px;
+ border-color: $border-color;
+ cursor: pointer;
+
+ &:hover {
+ background-color: #dce0e5;
+ }
+
+ &.active {
+ border-color: $border-gray-light;
+ background-color: $gray-light;
+
+ &:hover {
+ background-color: #dce0e5;
+ }
+
+ .counter {
+ font-weight: bold;
+ }
+ }
+
+ .icon {
+ float: left;
+ margin-right: 10px;
+ }
+
+ .counter {
+ float: left;
+ }
+ }
+
+ .awards-controls {
+ position: relative;
+ margin-left: 10px;
+ float: left;
+
+ .add-award {
+ font-size: 24px;
+ color: $gl-gray;
+ position: relative;
+ top: 2px;
+
+ &:hover,
+ &:link {
+ text-decoration: none;
+ }
+ }
+
+ .emoji-menu{
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ font-size: 14px;
+ text-align: left;
+ list-style: none;
+ background-color: #fff;
+ -webkit-background-clip: padding-box;
+ background-clip: padding-box;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0,0,0,.15);
+ border-radius: 4px;
+ -webkit-box-shadow: 0 6px 12px rgba(0,0,0,.175);
+ box-shadow: 0 6px 12px rgba(0,0,0,.175);
+
+ .emoji-menu-content {
+ padding: $gl-padding;
+ width: 300px;
+ height: 300px;
+ overflow-y: scroll;
+
+ h5 {
+ clear: left;
+ }
+
+ ul {
+ list-style-type: none;
+ margin-left: -20px;
+ margin-bottom: 20px;
+ overflow: auto;
+ }
+
+ input.emoji-search{
+ background: image-url("icon-search.png") 240px no-repeat;
+ }
+
+ li {
+ cursor: pointer;
+ width: 30px;
+ height: 30px;
+ text-align: center;
+ float: left;
+ margin: 3px;
+ list-decorate: none;
+ @include border-radius(5px);
+
+ &:hover {
+ background-color: #ccc;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 74dc3e321c1..3c2997c1d5a 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -21,7 +21,7 @@
.autoscroll-container {
position: fixed;
- bottom: 10px;
+ bottom: 20px;
right: 20px;
z-index: 100;
}
@@ -34,7 +34,7 @@
a {
display: block;
- margin-bottom: 5px;
+ margin-bottom: 10px;
}
}
@@ -67,9 +67,4 @@
color: #3084bb !important;
}
}
-
- .build-top-menu {
- margin-top: 0;
- margin-bottom: 2px;
- }
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index fbd7c363de1..17245d3be7b 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -2,10 +2,6 @@
display: block;
}
-.commit-title{
- margin-bottom: 10px;
-}
-
.commit-author, .commit-committer{
display: block;
color: #999;
@@ -41,6 +37,8 @@
.commit-box {
.commit-title {
margin: 0;
+ font-size: 23px;
+ color: #313236;
}
.commit-description {
@@ -56,6 +54,7 @@
li {
padding: 3px 0px;
+ line-height: 20px;
}
}
.new-file {
@@ -107,16 +106,3 @@
z-index: 2;
}
}
-
-.commit-ci-menu {
- padding: 0;
- margin: 0;
- list-style: none;
- margin-top: 5px;
- height: 56px;
- margin: -16px;
- padding: 16px;
- text-align: center;
- margin-top: 0px;
- margin-bottom: 2px;
-}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 4e121b95d13..879bd287470 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -33,6 +33,8 @@
}
li.commit {
+ list-style: none;
+
.commit-row-title {
font-size: $list-font-size;
line-height: 20px;
@@ -113,3 +115,66 @@ li.commit {
}
}
}
+
+.branch-commit {
+ color: $gl-gray;
+ .commit-id, .commit-row-message {
+ color: $gl-gray;
+ }
+}
+
+.divergence-graph {
+ padding: 12px 12px 0 0;
+ float: right;
+
+ .graph-side {
+ position: relative;
+ width: 80px;
+ height: 22px;
+ padding: 5px 0 13px;
+ float: left;
+
+ .bar {
+ position: absolute;
+ height: 4px;
+ background-color: #ccc;
+ }
+
+ .bar-behind {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .bar-ahead {
+ left: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .count {
+ padding-top: 6px;
+ padding-bottom: 0px;
+ font-size: 12px;
+ color: #333;
+ display: block;
+ }
+
+ .count-behind {
+ padding-right: 4px;
+ text-align: right;
+ }
+
+ .count-ahead {
+ padding-left: 4px;
+ text-align: left;
+ }
+ }
+
+ .graph-separator {
+ position: relative;
+ width: 1px;
+ height: 18px;
+ margin: 5px 0 0;
+ float: left;
+ background-color: #ccc;
+ }
+}
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
new file mode 100644
index 00000000000..deab805dbc2
--- /dev/null
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -0,0 +1,33 @@
+.detail-page-header {
+ margin: -$gl-padding;
+ padding: 7px $gl-padding;
+ margin-bottom: 0px;
+ border-bottom: 1px solid $border-color;
+ color: #5c5d5e;
+ font-size: 16px;
+ line-height: 34px;
+
+ .author {
+ color: #5c5d5e;
+ }
+
+ .identifier {
+ color: #5c5d5e;
+ }
+}
+
+.detail-page-description {
+ .title {
+ margin: 0;
+ font-size: 23px;
+ color: #313236;
+ }
+
+ .description {
+ margin-top: 6px;
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index d9ef06dc6b6..afd6fb73675 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -367,7 +367,6 @@
.inline-parallel-buttons {
float: right;
- margin-top: -5px;
}
// Mobile
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 1d565477dd4..39d916cd336 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -19,48 +19,38 @@
color: #B94A48;
}
}
- .commit-button-annotation {
- display: inline-block;
- margin: 0;
- padding: 2px;
-
- > * {
- float: left;
- }
-
- .message {
- display: inline-block;
- margin: 5px 8px 0 8px;
- }
- }
.file-title {
@extend .monospace;
+
+ line-height: 42px;
+ padding-top: 7px;
+ padding-bottom: 7px;
}
.editor-ref {
background: $background-color;
- padding: 11px 15px;
+ padding-right: $gl-padding;
border-right: 1px solid $border-color;
- display: inline-block;
- margin: -5px -5px;
+ display: block;
+ float: left;
margin-right: 10px;
}
.editor-file-name {
- .new-file-name {
- display: inline-block;
- width: 200px;
- }
+ @extend .monospace;
+
+ float: left;
+ margin-right: 10px;
+ }
- .form-control {
- margin-top: -3px;
- }
+ .new-file-name {
+ display: inline-block;
+ width: 450px;
+ float: left;
}
- .form-actions {
- margin: -$gl-padding;
- margin-top: 0;
- padding: $gl-padding
+ .select2 {
+ float: right;
}
}
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/pages/emojis.scss
new file mode 100644
index 00000000000..89a94c5a780
--- /dev/null
+++ b/app/assets/stylesheets/pages/emojis.scss
@@ -0,0 +1,1272 @@
+/*
+File is generated by https://github.com/jakesgordon/sprite-factory and midified manualy
+The source: gemojione gem.
+*/
+
+.emoji-icon{
+ background-image: image-url("emoji.png");
+ background-repeat: no-repeat;
+}
+
+.emoji-0023-20E3 { background-position: 0px 0px; }
+.emoji-0030-20E3 { background-position: -20px 0px; }
+.emoji-0031-20E3 { background-position: -40px 0px; }
+.emoji-0032-20E3 { background-position: -60px 0px; }
+.emoji-0033-20E3 { background-position: -80px 0px; }
+.emoji-0034-20E3 { background-position: -100px 0px; }
+.emoji-0035-20E3 { background-position: -120px 0px; }
+.emoji-0036-20E3 { background-position: -140px 0px; }
+.emoji-0037-20E3 { background-position: -160px 0px; }
+.emoji-0038-20E3 { background-position: -180px 0px; }
+.emoji-0039-20E3 { background-position: -200px 0px; }
+.emoji-00A9 { background-position: -220px 0px; }
+.emoji-00AE { background-position: -240px 0px; }
+.emoji-1F004 { background-position: -260px 0px; }
+.emoji-1F0CF { background-position: -280px 0px; }
+.emoji-1F170 { background-position: -300px 0px; }
+.emoji-1F171 { background-position: -320px 0px; }
+.emoji-1F17E { background-position: -340px 0px; }
+.emoji-1F17F { background-position: -360px 0px; }
+.emoji-1F18E { background-position: -380px 0px; }
+.emoji-1F191 { background-position: -400px 0px; }
+.emoji-1F192 { background-position: -420px 0px; }
+.emoji-1F193 { background-position: -440px 0px; }
+.emoji-1F194 { background-position: -460px 0px; }
+.emoji-1F195 { background-position: -480px 0px; }
+.emoji-1F196 { background-position: -500px 0px; }
+.emoji-1F197 { background-position: -520px 0px; }
+.emoji-1F198 { background-position: -540px 0px; }
+.emoji-1F199 { background-position: -560px 0px; }
+.emoji-1F19A { background-position: -580px 0px; }
+.emoji-1F1E6-1F1E8 { background-position: -600px 0px; }
+.emoji-1F1E6-1F1E9 { background-position: -620px 0px; }
+.emoji-1F1E6-1F1EA { background-position: -640px 0px; }
+.emoji-1F1E6-1F1EB { background-position: -660px 0px; }
+.emoji-1F1E6-1F1EC { background-position: -680px 0px; }
+.emoji-1F1E6-1F1EE { background-position: -700px 0px; }
+.emoji-1F1E6-1F1F1 { background-position: -720px 0px; }
+.emoji-1F1E6-1F1F2 { background-position: -740px 0px; }
+.emoji-1F1E6-1F1F4 { background-position: -760px 0px; }
+.emoji-1F1E6-1F1F7 { background-position: -780px 0px; }
+.emoji-1F1E6-1F1F9 { background-position: -800px 0px; }
+.emoji-1F1E6-1F1FA { background-position: -820px 0px; }
+.emoji-1F1E6-1F1FC { background-position: -840px 0px; }
+.emoji-1F1E6-1F1FF { background-position: -860px 0px; }
+.emoji-1F1E7-1F1E6 { background-position: -880px 0px; }
+.emoji-1F1E7-1F1E7 { background-position: -900px 0px; }
+.emoji-1F1E7-1F1E9 { background-position: -920px 0px; }
+.emoji-1F1E7-1F1EA { background-position: -940px 0px; }
+.emoji-1F1E7-1F1EB { background-position: -960px 0px; }
+.emoji-1F1E7-1F1EC { background-position: -980px 0px; }
+.emoji-1F1E7-1F1ED { background-position: -1000px 0px; }
+.emoji-1F1E7-1F1EE { background-position: -1020px 0px; }
+.emoji-1F1E7-1F1EF { background-position: -1040px 0px; }
+.emoji-1F1E7-1F1F2 { background-position: -1060px 0px; }
+.emoji-1F1E7-1F1F3 { background-position: -1080px 0px; }
+.emoji-1F1E7-1F1F4 { background-position: -1100px 0px; }
+.emoji-1F1E7-1F1F7 { background-position: -1120px 0px; }
+.emoji-1F1E7-1F1F8 { background-position: -1140px 0px; }
+.emoji-1F1E7-1F1F9 { background-position: -1160px 0px; }
+.emoji-1F1E7-1F1FC { background-position: -1180px 0px; }
+.emoji-1F1E7-1F1FE { background-position: -1200px 0px; }
+.emoji-1F1E7-1F1FF { background-position: -1220px 0px; }
+.emoji-1F1E8-1F1E6 { background-position: -1240px 0px; }
+.emoji-1F1E8-1F1E9 { background-position: -1260px 0px; }
+.emoji-1F1E8-1F1EB { background-position: -1280px 0px; }
+.emoji-1F1E8-1F1EC { background-position: -1300px 0px; }
+.emoji-1F1E8-1F1ED { background-position: -1320px 0px; }
+.emoji-1F1E8-1F1EE { background-position: -1340px 0px; }
+.emoji-1F1E8-1F1F1 { background-position: -1360px 0px; }
+.emoji-1F1E8-1F1F2 { background-position: -1380px 0px; }
+.emoji-1F1E8-1F1F3 { background-position: -1400px 0px; }
+.emoji-1F1E8-1F1F4 { background-position: -1420px 0px; }
+.emoji-1F1E8-1F1F7 { background-position: -1440px 0px; }
+.emoji-1F1E8-1F1FA { background-position: -1460px 0px; }
+.emoji-1F1E8-1F1FB { background-position: -1480px 0px; }
+.emoji-1F1E8-1F1FE { background-position: -1500px 0px; }
+.emoji-1F1E8-1F1FF { background-position: -1520px 0px; }
+.emoji-1F1E9-1F1EA { background-position: -1540px 0px; }
+.emoji-1F1E9-1F1EF { background-position: -1560px 0px; }
+.emoji-1F1E9-1F1F0 { background-position: -1580px 0px; }
+.emoji-1F1E9-1F1F2 { background-position: -1600px 0px; }
+.emoji-1F1E9-1F1F4 { background-position: -1620px 0px; }
+.emoji-1F1E9-1F1FF { background-position: -1640px 0px; }
+.emoji-1F1EA-1F1E8 { background-position: -1660px 0px; }
+.emoji-1F1EA-1F1EA { background-position: -1680px 0px; }
+.emoji-1F1EA-1F1EC { background-position: -1700px 0px; }
+.emoji-1F1EA-1F1ED { background-position: -1720px 0px; }
+.emoji-1F1EA-1F1F7 { background-position: -1740px 0px; }
+.emoji-1F1EA-1F1F8 { background-position: -1760px 0px; }
+.emoji-1F1EA-1F1F9 { background-position: -1780px 0px; }
+.emoji-1F1EB-1F1EE { background-position: -1800px 0px; }
+.emoji-1F1EB-1F1EF { background-position: -1820px 0px; }
+.emoji-1F1EB-1F1F0 { background-position: -1840px 0px; }
+.emoji-1F1EB-1F1F2 { background-position: -1860px 0px; }
+.emoji-1F1EB-1F1F4 { background-position: -1880px 0px; }
+.emoji-1F1EB-1F1F7 { background-position: -1900px 0px; }
+.emoji-1F1EC-1F1E6 { background-position: -1920px 0px; }
+.emoji-1F1EC-1F1E7 { background-position: -1940px 0px; }
+.emoji-1F1EC-1F1E9 { background-position: -1960px 0px; }
+.emoji-1F1EC-1F1EA { background-position: -1980px 0px; }
+.emoji-1F1EC-1F1ED { background-position: -2000px 0px; }
+.emoji-1F1EC-1F1EE { background-position: -2020px 0px; }
+.emoji-1F1EC-1F1F1 { background-position: -2040px 0px; }
+.emoji-1F1EC-1F1F2 { background-position: -2060px 0px; }
+.emoji-1F1EC-1F1F3 { background-position: -2080px 0px; }
+.emoji-1F1EC-1F1F6 { background-position: -2100px 0px; }
+.emoji-1F1EC-1F1F7 { background-position: -2120px 0px; }
+.emoji-1F1EC-1F1F9 { background-position: -2140px 0px; }
+.emoji-1F1EC-1F1FA { background-position: -2160px 0px; }
+.emoji-1F1EC-1F1FC { background-position: -2180px 0px; }
+.emoji-1F1EC-1F1FE { background-position: -2200px 0px; }
+.emoji-1F1ED-1F1F0 { background-position: -2220px 0px; }
+.emoji-1F1ED-1F1F3 { background-position: -2240px 0px; }
+.emoji-1F1ED-1F1F7 { background-position: -2260px 0px; }
+.emoji-1F1ED-1F1F9 { background-position: -2280px 0px; }
+.emoji-1F1ED-1F1FA { background-position: -2300px 0px; }
+.emoji-1F1EE-1F1E9 { background-position: -2320px 0px; }
+.emoji-1F1EE-1F1EA { background-position: -2340px 0px; }
+.emoji-1F1EE-1F1F1 { background-position: -2360px 0px; }
+.emoji-1F1EE-1F1F3 { background-position: -2380px 0px; }
+.emoji-1F1EE-1F1F6 { background-position: -2400px 0px; }
+.emoji-1F1EE-1F1F7 { background-position: -2420px 0px; }
+.emoji-1F1EE-1F1F8 { background-position: -2440px 0px; }
+.emoji-1F1EE-1F1F9 { background-position: -2460px 0px; }
+.emoji-1F1EF-1F1EA { background-position: -2480px 0px; }
+.emoji-1F1EF-1F1F2 { background-position: -2500px 0px; }
+.emoji-1F1EF-1F1F4 { background-position: -2520px 0px; }
+.emoji-1F1EF-1F1F5 { background-position: -2540px 0px; }
+.emoji-1F1F0-1F1EA { background-position: -2560px 0px; }
+.emoji-1F1F0-1F1EC { background-position: -2580px 0px; }
+.emoji-1F1F0-1F1ED { background-position: -2600px 0px; }
+.emoji-1F1F0-1F1EE { background-position: -2620px 0px; }
+.emoji-1F1F0-1F1F2 { background-position: -2640px 0px; }
+.emoji-1F1F0-1F1F3 { background-position: -2660px 0px; }
+.emoji-1F1F0-1F1F5 { background-position: -2680px 0px; }
+.emoji-1F1F0-1F1F7 { background-position: -2700px 0px; }
+.emoji-1F1F0-1F1FC { background-position: -2720px 0px; }
+.emoji-1F1F0-1F1FE { background-position: -2740px 0px; }
+.emoji-1F1F0-1F1FF { background-position: -2760px 0px; }
+.emoji-1F1F1-1F1E6 { background-position: -2780px 0px; }
+.emoji-1F1F1-1F1E7 { background-position: -2800px 0px; }
+.emoji-1F1F1-1F1E8 { background-position: -2820px 0px; }
+.emoji-1F1F1-1F1EE { background-position: -2840px 0px; }
+.emoji-1F1F1-1F1F0 { background-position: -2860px 0px; }
+.emoji-1F1F1-1F1F7 { background-position: -2880px 0px; }
+.emoji-1F1F1-1F1F8 { background-position: -2900px 0px; }
+.emoji-1F1F1-1F1F9 { background-position: -2920px 0px; }
+.emoji-1F1F1-1F1FA { background-position: -2940px 0px; }
+.emoji-1F1F1-1F1FB { background-position: -2960px 0px; }
+.emoji-1F1F1-1F1FE { background-position: -2980px 0px; }
+.emoji-1F1F2-1F1E6 { background-position: -3000px 0px; }
+.emoji-1F1F2-1F1E8 { background-position: -3020px 0px; }
+.emoji-1F1F2-1F1E9 { background-position: -3040px 0px; }
+.emoji-1F1F2-1F1EA { background-position: -3060px 0px; }
+.emoji-1F1F2-1F1EC { background-position: -3080px 0px; }
+.emoji-1F1F2-1F1ED { background-position: -3100px 0px; }
+.emoji-1F1F2-1F1F0 { background-position: -3120px 0px; }
+.emoji-1F1F2-1F1F1 { background-position: -3140px 0px; }
+.emoji-1F1F2-1F1F2 { background-position: -3160px 0px; }
+.emoji-1F1F2-1F1F3 { background-position: -3180px 0px; }
+.emoji-1F1F2-1F1F4 { background-position: -3200px 0px; }
+.emoji-1F1F2-1F1F7 { background-position: -3220px 0px; }
+.emoji-1F1F2-1F1F8 { background-position: -3240px 0px; }
+.emoji-1F1F2-1F1F9 { background-position: -3260px 0px; }
+.emoji-1F1F2-1F1FA { background-position: -3280px 0px; }
+.emoji-1F1F2-1F1FB { background-position: -3300px 0px; }
+.emoji-1F1F2-1F1FC { background-position: -3320px 0px; }
+.emoji-1F1F2-1F1FD { background-position: -3340px 0px; }
+.emoji-1F1F2-1F1FE { background-position: -3360px 0px; }
+.emoji-1F1F2-1F1FF { background-position: -3380px 0px; }
+.emoji-1F1F3-1F1E6 { background-position: -3400px 0px; }
+.emoji-1F1F3-1F1E8 { background-position: -3420px 0px; }
+.emoji-1F1F3-1F1EA { background-position: -3440px 0px; }
+.emoji-1F1F3-1F1EC { background-position: -3460px 0px; }
+.emoji-1F1F3-1F1EE { background-position: -3480px 0px; }
+.emoji-1F1F3-1F1F1 { background-position: -3500px 0px; }
+.emoji-1F1F3-1F1F4 { background-position: -3520px 0px; }
+.emoji-1F1F3-1F1F5 { background-position: -3540px 0px; }
+.emoji-1F1F3-1F1F7 { background-position: -3560px 0px; }
+.emoji-1F1F3-1F1FA { background-position: -3580px 0px; }
+.emoji-1F1F3-1F1FF { background-position: -3600px 0px; }
+.emoji-1F1F4-1F1F2 { background-position: -3620px 0px; }
+.emoji-1F1F5-1F1E6 { background-position: -3640px 0px; }
+.emoji-1F1F5-1F1EA { background-position: -3660px 0px; }
+.emoji-1F1F5-1F1EB { background-position: -3680px 0px; }
+.emoji-1F1F5-1F1EC { background-position: -3700px 0px; }
+.emoji-1F1F5-1F1ED { background-position: -3720px 0px; }
+.emoji-1F1F5-1F1F0 { background-position: -3740px 0px; }
+.emoji-1F1F5-1F1F1 { background-position: -3760px 0px; }
+.emoji-1F1F5-1F1F7 { background-position: -3780px 0px; }
+.emoji-1F1F5-1F1F8 { background-position: -3800px 0px; }
+.emoji-1F1F5-1F1F9 { background-position: -3820px 0px; }
+.emoji-1F1F5-1F1FC { background-position: -3840px 0px; }
+.emoji-1F1F5-1F1FE { background-position: -3860px 0px; }
+.emoji-1F1F6-1F1E6 { background-position: -3880px 0px; }
+.emoji-1F1F7-1F1F4 { background-position: -3900px 0px; }
+.emoji-1F1F7-1F1F8 { background-position: -3920px 0px; }
+.emoji-1F1F7-1F1FA { background-position: -3940px 0px; }
+.emoji-1F1F7-1F1FC { background-position: -3960px 0px; }
+.emoji-1F1F8-1F1E6 { background-position: -3980px 0px; }
+.emoji-1F1F8-1F1E7 { background-position: -4000px 0px; }
+.emoji-1F1F8-1F1E8 { background-position: -4020px 0px; }
+.emoji-1F1F8-1F1E9 { background-position: -4040px 0px; }
+.emoji-1F1F8-1F1EA { background-position: -4060px 0px; }
+.emoji-1F1F8-1F1EC { background-position: -4080px 0px; }
+.emoji-1F1F8-1F1ED { background-position: -4100px 0px; }
+.emoji-1F1F8-1F1EE { background-position: -4120px 0px; }
+.emoji-1F1F8-1F1F0 { background-position: -4140px 0px; }
+.emoji-1F1F8-1F1F1 { background-position: -4160px 0px; }
+.emoji-1F1F8-1F1F2 { background-position: -4180px 0px; }
+.emoji-1F1F8-1F1F3 { background-position: -4200px 0px; }
+.emoji-1F1F8-1F1F4 { background-position: -4220px 0px; }
+.emoji-1F1F8-1F1F7 { background-position: -4240px 0px; }
+.emoji-1F1F8-1F1F9 { background-position: -4260px 0px; }
+.emoji-1F1F8-1F1FB { background-position: -4280px 0px; }
+.emoji-1F1F8-1F1FE { background-position: -4300px 0px; }
+.emoji-1F1F8-1F1FF { background-position: -4320px 0px; }
+.emoji-1F1F9-1F1E9 { background-position: -4340px 0px; }
+.emoji-1F1F9-1F1EC { background-position: -4360px 0px; }
+.emoji-1F1F9-1F1ED { background-position: -4380px 0px; }
+.emoji-1F1F9-1F1EF { background-position: -4400px 0px; }
+.emoji-1F1F9-1F1F1 { background-position: -4420px 0px; }
+.emoji-1F1F9-1F1F2 { background-position: -4440px 0px; }
+.emoji-1F1F9-1F1F3 { background-position: -4460px 0px; }
+.emoji-1F1F9-1F1F4 { background-position: -4480px 0px; }
+.emoji-1F1F9-1F1F7 { background-position: -4500px 0px; }
+.emoji-1F1F9-1F1F9 { background-position: -4520px 0px; }
+.emoji-1F1F9-1F1FB { background-position: -4540px 0px; }
+.emoji-1F1F9-1F1FC { background-position: -4560px 0px; }
+.emoji-1F1F9-1F1FF { background-position: -4580px 0px; }
+.emoji-1F1FA-1F1E6 { background-position: -4600px 0px; }
+.emoji-1F1FA-1F1EC { background-position: -4620px 0px; }
+.emoji-1F1FA-1F1F8 { background-position: -4640px 0px; }
+.emoji-1F1FA-1F1FE { background-position: -4660px 0px; }
+.emoji-1F1FA-1F1FF { background-position: -4680px 0px; }
+.emoji-1F1FB-1F1E6 { background-position: -4700px 0px; }
+.emoji-1F1FB-1F1E8 { background-position: -4720px 0px; }
+.emoji-1F1FB-1F1EA { background-position: -4740px 0px; }
+.emoji-1F1FB-1F1EE { background-position: -4760px 0px; }
+.emoji-1F1FB-1F1F3 { background-position: -4780px 0px; }
+.emoji-1F1FB-1F1FA { background-position: -4800px 0px; }
+.emoji-1F1FC-1F1EB { background-position: -4820px 0px; }
+.emoji-1F1FC-1F1F8 { background-position: -4840px 0px; }
+.emoji-1F1FD-1F1F0 { background-position: -4860px 0px; }
+.emoji-1F1FE-1F1EA { background-position: -4880px 0px; }
+.emoji-1F1FF-1F1E6 { background-position: -4900px 0px; }
+.emoji-1F1FF-1F1F2 { background-position: -4920px 0px; }
+.emoji-1F1FF-1F1FC { background-position: -4940px 0px; }
+.emoji-1F201 { background-position: -4960px 0px; }
+.emoji-1F202 { background-position: -4980px 0px; }
+.emoji-1F21A { background-position: -5000px 0px; }
+.emoji-1F22F { background-position: -5020px 0px; }
+.emoji-1F232 { background-position: -5040px 0px; }
+.emoji-1F233 { background-position: -5060px 0px; }
+.emoji-1F234 { background-position: -5080px 0px; }
+.emoji-1F235 { background-position: -5100px 0px; }
+.emoji-1F236 { background-position: -5120px 0px; }
+.emoji-1F237 { background-position: -5140px 0px; }
+.emoji-1F238 { background-position: -5160px 0px; }
+.emoji-1F239 { background-position: -5180px 0px; }
+.emoji-1F23A { background-position: -5200px 0px; }
+.emoji-1F250 { background-position: -5220px 0px; }
+.emoji-1F251 { background-position: -5240px 0px; }
+.emoji-1F300 { background-position: -5260px 0px; }
+.emoji-1F301 { background-position: -5280px 0px; }
+.emoji-1F302 { background-position: -5300px 0px; }
+.emoji-1F303 { background-position: -5320px 0px; }
+.emoji-1F304 { background-position: -5340px 0px; }
+.emoji-1F305 { background-position: -5360px 0px; }
+.emoji-1F306 { background-position: -5380px 0px; }
+.emoji-1F307 { background-position: -5400px 0px; }
+.emoji-1F308 { background-position: -5420px 0px; }
+.emoji-1F309 { background-position: -5440px 0px; }
+.emoji-1F30A { background-position: -5460px 0px; }
+.emoji-1F30B { background-position: -5480px 0px; }
+.emoji-1F30C { background-position: -5500px 0px; }
+.emoji-1F30D { background-position: -5520px 0px; }
+.emoji-1F30E { background-position: -5540px 0px; }
+.emoji-1F30F { background-position: -5560px 0px; }
+.emoji-1F310 { background-position: -5580px 0px; }
+.emoji-1F311 { background-position: -5600px 0px; }
+.emoji-1F312 { background-position: -5620px 0px; }
+.emoji-1F313 { background-position: -5640px 0px; }
+.emoji-1F314 { background-position: -5660px 0px; }
+.emoji-1F315 { background-position: -5680px 0px; }
+.emoji-1F316 { background-position: -5700px 0px; }
+.emoji-1F317 { background-position: -5720px 0px; }
+.emoji-1F318 { background-position: -5740px 0px; }
+.emoji-1F319 { background-position: -5760px 0px; }
+.emoji-1F31A { background-position: -5780px 0px; }
+.emoji-1F31B { background-position: -5800px 0px; }
+.emoji-1F31C { background-position: -5820px 0px; }
+.emoji-1F31D { background-position: -5840px 0px; }
+.emoji-1F31E { background-position: -5860px 0px; }
+.emoji-1F31F { background-position: -5880px 0px; }
+.emoji-1F320 { background-position: -5900px 0px; }
+.emoji-1F321 { background-position: -5920px 0px; }
+.emoji-1F327 { background-position: -5940px 0px; }
+.emoji-1F328 { background-position: -5960px 0px; }
+.emoji-1F329 { background-position: -5980px 0px; }
+.emoji-1F32A { background-position: -6000px 0px; }
+.emoji-1F32B { background-position: -6020px 0px; }
+.emoji-1F32C { background-position: -6040px 0px; }
+.emoji-1F330 { background-position: -6060px 0px; }
+.emoji-1F331 { background-position: -6080px 0px; }
+.emoji-1F332 { background-position: -6100px 0px; }
+.emoji-1F333 { background-position: -6120px 0px; }
+.emoji-1F334 { background-position: -6140px 0px; }
+.emoji-1F335 { background-position: -6160px 0px; }
+.emoji-1F336 { background-position: -6180px 0px; }
+.emoji-1F337 { background-position: -6200px 0px; }
+.emoji-1F338 { background-position: -6220px 0px; }
+.emoji-1F339 { background-position: -6240px 0px; }
+.emoji-1F33A { background-position: -6260px 0px; }
+.emoji-1F33B { background-position: -6280px 0px; }
+.emoji-1F33C { background-position: -6300px 0px; }
+.emoji-1F33D { background-position: -6320px 0px; }
+.emoji-1F33E { background-position: -6340px 0px; }
+.emoji-1F33F { background-position: -6360px 0px; }
+.emoji-1F340 { background-position: -6380px 0px; }
+.emoji-1F341 { background-position: -6400px 0px; }
+.emoji-1F342 { background-position: -6420px 0px; }
+.emoji-1F343 { background-position: -6440px 0px; }
+.emoji-1F344 { background-position: -6460px 0px; }
+.emoji-1F345 { background-position: -6480px 0px; }
+.emoji-1F346 { background-position: -6500px 0px; }
+.emoji-1F347 { background-position: -6520px 0px; }
+.emoji-1F348 { background-position: -6540px 0px; }
+.emoji-1F349 { background-position: -6560px 0px; }
+.emoji-1F34A { background-position: -6580px 0px; }
+.emoji-1F34B { background-position: -6600px 0px; }
+.emoji-1F34C { background-position: -6620px 0px; }
+.emoji-1F34D { background-position: -6640px 0px; }
+.emoji-1F34E { background-position: -6660px 0px; }
+.emoji-1F34F { background-position: -6680px 0px; }
+.emoji-1F350 { background-position: -6700px 0px; }
+.emoji-1F351 { background-position: -6720px 0px; }
+.emoji-1F352 { background-position: -6740px 0px; }
+.emoji-1F353 { background-position: -6760px 0px; }
+.emoji-1F354 { background-position: -6780px 0px; }
+.emoji-1F355 { background-position: -6800px 0px; }
+.emoji-1F356 { background-position: -6820px 0px; }
+.emoji-1F357 { background-position: -6840px 0px; }
+.emoji-1F358 { background-position: -6860px 0px; }
+.emoji-1F359 { background-position: -6880px 0px; }
+.emoji-1F35A { background-position: -6900px 0px; }
+.emoji-1F35B { background-position: -6920px 0px; }
+.emoji-1F35C { background-position: -6940px 0px; }
+.emoji-1F35D { background-position: -6960px 0px; }
+.emoji-1F35E { background-position: -6980px 0px; }
+.emoji-1F35F { background-position: -7000px 0px; }
+.emoji-1F360 { background-position: -7020px 0px; }
+.emoji-1F361 { background-position: -7040px 0px; }
+.emoji-1F362 { background-position: -7060px 0px; }
+.emoji-1F363 { background-position: -7080px 0px; }
+.emoji-1F364 { background-position: -7100px 0px; }
+.emoji-1F365 { background-position: -7120px 0px; }
+.emoji-1F366 { background-position: -7140px 0px; }
+.emoji-1F367 { background-position: -7160px 0px; }
+.emoji-1F368 { background-position: -7180px 0px; }
+.emoji-1F369 { background-position: -7200px 0px; }
+.emoji-1F36A { background-position: -7220px 0px; }
+.emoji-1F36B { background-position: -7240px 0px; }
+.emoji-1F36C { background-position: -7260px 0px; }
+.emoji-1F36D { background-position: -7280px 0px; }
+.emoji-1F36E { background-position: -7300px 0px; }
+.emoji-1F36F { background-position: -7320px 0px; }
+.emoji-1F370 { background-position: -7340px 0px; }
+.emoji-1F371 { background-position: -7360px 0px; }
+.emoji-1F372 { background-position: -7380px 0px; }
+.emoji-1F373 { background-position: -7400px 0px; }
+.emoji-1F374 { background-position: -7420px 0px; }
+.emoji-1F375 { background-position: -7440px 0px; }
+.emoji-1F376 { background-position: -7460px 0px; }
+.emoji-1F377 { background-position: -7480px 0px; }
+.emoji-1F378 { background-position: -7500px 0px; }
+.emoji-1F379 { background-position: -7520px 0px; }
+.emoji-1F37A { background-position: -7540px 0px; }
+.emoji-1F37B { background-position: -7560px 0px; }
+.emoji-1F37C { background-position: -7580px 0px; }
+.emoji-1F37D { background-position: -7600px 0px; }
+.emoji-1F380 { background-position: -7620px 0px; }
+.emoji-1F381 { background-position: -7640px 0px; }
+.emoji-1F382 { background-position: -7660px 0px; }
+.emoji-1F383 { background-position: -7680px 0px; }
+.emoji-1F384 { background-position: -7700px 0px; }
+.emoji-1F385 { background-position: -7720px 0px; }
+.emoji-1F386 { background-position: -7740px 0px; }
+.emoji-1F387 { background-position: -7760px 0px; }
+.emoji-1F388 { background-position: -7780px 0px; }
+.emoji-1F389 { background-position: -7800px 0px; }
+.emoji-1F38A { background-position: -7820px 0px; }
+.emoji-1F38B { background-position: -7840px 0px; }
+.emoji-1F38C { background-position: -7860px 0px; }
+.emoji-1F38D { background-position: -7880px 0px; }
+.emoji-1F38E { background-position: -7900px 0px; }
+.emoji-1F38F { background-position: -7920px 0px; }
+.emoji-1F390 { background-position: -7940px 0px; }
+.emoji-1F391 { background-position: -7960px 0px; }
+.emoji-1F392 { background-position: -7980px 0px; }
+.emoji-1F393 { background-position: -8000px 0px; }
+.emoji-1F394 { background-position: -8020px 0px; }
+.emoji-1F395 { background-position: -8040px 0px; }
+.emoji-1F396 { background-position: -8060px 0px; }
+.emoji-1F397 { background-position: -8080px 0px; }
+.emoji-1F398 { background-position: -8100px 0px; }
+.emoji-1F399 { background-position: -8120px 0px; }
+.emoji-1F39A { background-position: -8140px 0px; }
+.emoji-1F39B { background-position: -8160px 0px; }
+.emoji-1F39C { background-position: -8180px 0px; }
+.emoji-1F39D { background-position: -8200px 0px; }
+.emoji-1F39E { background-position: -8220px 0px; }
+.emoji-1F39F { background-position: -8240px 0px; }
+.emoji-1F3A0 { background-position: -8260px 0px; }
+.emoji-1F3A1 { background-position: -8280px 0px; }
+.emoji-1F3A2 { background-position: -8300px 0px; }
+.emoji-1F3A3 { background-position: -8320px 0px; }
+.emoji-1F3A4 { background-position: -8340px 0px; }
+.emoji-1F3A5 { background-position: -8360px 0px; }
+.emoji-1F3A6 { background-position: -8380px 0px; }
+.emoji-1F3A7 { background-position: -8400px 0px; }
+.emoji-1F3A8 { background-position: -8420px 0px; }
+.emoji-1F3A9 { background-position: -8440px 0px; }
+.emoji-1F3AA { background-position: -8460px 0px; }
+.emoji-1F3AB { background-position: -8480px 0px; }
+.emoji-1F3AC { background-position: -8500px 0px; }
+.emoji-1F3AD { background-position: -8520px 0px; }
+.emoji-1F3AE { background-position: -8540px 0px; }
+.emoji-1F3AF { background-position: -8560px 0px; }
+.emoji-1F3B0 { background-position: -8580px 0px; }
+.emoji-1F3B1 { background-position: -8600px 0px; }
+.emoji-1F3B2 { background-position: -8620px 0px; }
+.emoji-1F3B3 { background-position: -8640px 0px; }
+.emoji-1F3B4 { background-position: -8660px 0px; }
+.emoji-1F3B5 { background-position: -8680px 0px; }
+.emoji-1F3B6 { background-position: -8700px 0px; }
+.emoji-1F3B7 { background-position: -8720px 0px; }
+.emoji-1F3B8 { background-position: -8740px 0px; }
+.emoji-1F3B9 { background-position: -8760px 0px; }
+.emoji-1F3BA { background-position: -8780px 0px; }
+.emoji-1F3BB { background-position: -8800px 0px; }
+.emoji-1F3BC { background-position: -8820px 0px; }
+.emoji-1F3BD { background-position: -8840px 0px; }
+.emoji-1F3BE { background-position: -8860px 0px; }
+.emoji-1F3BF { background-position: -8880px 0px; }
+.emoji-1F3C0 { background-position: -8900px 0px; }
+.emoji-1F3C1 { background-position: -8920px 0px; }
+.emoji-1F3C2 { background-position: -8940px 0px; }
+.emoji-1F3C3 { background-position: -8960px 0px; }
+.emoji-1F3C4 { background-position: -8980px 0px; }
+.emoji-1F3C5 { background-position: -9000px 0px; }
+.emoji-1F3C6 { background-position: -9020px 0px; }
+.emoji-1F3C7 { background-position: -9040px 0px; }
+.emoji-1F3C8 { background-position: -9060px 0px; }
+.emoji-1F3C9 { background-position: -9080px 0px; }
+.emoji-1F3CA { background-position: -9100px 0px; }
+.emoji-1F3CB { background-position: -9120px 0px; }
+.emoji-1F3CC { background-position: -9140px 0px; }
+.emoji-1F3CD { background-position: -9160px 0px; }
+.emoji-1F3CE { background-position: -9180px 0px; }
+.emoji-1F3D4 { background-position: -9200px 0px; }
+.emoji-1F3D5 { background-position: -9220px 0px; }
+.emoji-1F3D6 { background-position: -9240px 0px; }
+.emoji-1F3D7 { background-position: -9260px 0px; }
+.emoji-1F3D8 { background-position: -9280px 0px; }
+.emoji-1F3D9 { background-position: -9300px 0px; }
+.emoji-1F3DA { background-position: -9320px 0px; }
+.emoji-1F3DB { background-position: -9340px 0px; }
+.emoji-1F3DC { background-position: -9360px 0px; }
+.emoji-1F3DD { background-position: -9380px 0px; }
+.emoji-1F3DE { background-position: -9400px 0px; }
+.emoji-1F3DF { background-position: -9420px 0px; }
+.emoji-1F3E0 { background-position: -9440px 0px; }
+.emoji-1F3E1 { background-position: -9460px 0px; }
+.emoji-1F3E2 { background-position: -9480px 0px; }
+.emoji-1F3E3 { background-position: -9500px 0px; }
+.emoji-1F3E4 { background-position: -9520px 0px; }
+.emoji-1F3E5 { background-position: -9540px 0px; }
+.emoji-1F3E6 { background-position: -9560px 0px; }
+.emoji-1F3E7 { background-position: -9580px 0px; }
+.emoji-1F3E8 { background-position: -9600px 0px; }
+.emoji-1F3E9 { background-position: -9620px 0px; }
+.emoji-1F3EA { background-position: -9640px 0px; }
+.emoji-1F3EB { background-position: -9660px 0px; }
+.emoji-1F3EC { background-position: -9680px 0px; }
+.emoji-1F3ED { background-position: -9700px 0px; }
+.emoji-1F3EE { background-position: -9720px 0px; }
+.emoji-1F3EF { background-position: -9740px 0px; }
+.emoji-1F3F0 { background-position: -9760px 0px; }
+.emoji-1F3F1 { background-position: -9780px 0px; }
+.emoji-1F3F2 { background-position: -9800px 0px; }
+.emoji-1F3F3 { background-position: -9820px 0px; }
+.emoji-1F3F4 { background-position: -9840px 0px; }
+.emoji-1F3F5 { background-position: -9860px 0px; }
+.emoji-1F3F6 { background-position: -9880px 0px; }
+.emoji-1F3F7 { background-position: -9900px 0px; }
+.emoji-1F400 { background-position: -9920px 0px; }
+.emoji-1F401 { background-position: -9940px 0px; }
+.emoji-1F402 { background-position: -9960px 0px; }
+.emoji-1F403 { background-position: -9980px 0px; }
+.emoji-1F404 { background-position: -10000px 0px; }
+.emoji-1F405 { background-position: -10020px 0px; }
+.emoji-1F406 { background-position: -10040px 0px; }
+.emoji-1F407 { background-position: -10060px 0px; }
+.emoji-1F408 { background-position: -10080px 0px; }
+.emoji-1F409 { background-position: -10100px 0px; }
+.emoji-1F40A { background-position: -10120px 0px; }
+.emoji-1F40B { background-position: -10140px 0px; }
+.emoji-1F40C { background-position: -10160px 0px; }
+.emoji-1F40D { background-position: -10180px 0px; }
+.emoji-1F40E { background-position: -10200px 0px; }
+.emoji-1F40F { background-position: -10220px 0px; }
+.emoji-1F410 { background-position: -10240px 0px; }
+.emoji-1F411 { background-position: -10260px 0px; }
+.emoji-1F412 { background-position: -10280px 0px; }
+.emoji-1F413 { background-position: -10300px 0px; }
+.emoji-1F414 { background-position: -10320px 0px; }
+.emoji-1F415 { background-position: -10340px 0px; }
+.emoji-1F416 { background-position: -10360px 0px; }
+.emoji-1F417 { background-position: -10380px 0px; }
+.emoji-1F418 { background-position: -10400px 0px; }
+.emoji-1F419 { background-position: -10420px 0px; }
+.emoji-1F41A { background-position: -10440px 0px; }
+.emoji-1F41B { background-position: -10460px 0px; }
+.emoji-1F41C { background-position: -10480px 0px; }
+.emoji-1F41D { background-position: -10500px 0px; }
+.emoji-1F41E { background-position: -10520px 0px; }
+.emoji-1F41F { background-position: -10540px 0px; }
+.emoji-1F420 { background-position: -10560px 0px; }
+.emoji-1F421 { background-position: -10580px 0px; }
+.emoji-1F422 { background-position: -10600px 0px; }
+.emoji-1F423 { background-position: -10620px 0px; }
+.emoji-1F424 { background-position: -10640px 0px; }
+.emoji-1F425 { background-position: -10660px 0px; }
+.emoji-1F426 { background-position: -10680px 0px; }
+.emoji-1F427 { background-position: -10700px 0px; }
+.emoji-1F428 { background-position: -10720px 0px; }
+.emoji-1F429 { background-position: -10740px 0px; }
+.emoji-1F42A { background-position: -10760px 0px; }
+.emoji-1F42B { background-position: -10780px 0px; }
+.emoji-1F42C { background-position: -10800px 0px; }
+.emoji-1F42D { background-position: -10820px 0px; }
+.emoji-1F42E { background-position: -10840px 0px; }
+.emoji-1F42F { background-position: -10860px 0px; }
+.emoji-1F430 { background-position: -10880px 0px; }
+.emoji-1F431 { background-position: -10900px 0px; }
+.emoji-1F432 { background-position: -10920px 0px; }
+.emoji-1F433 { background-position: -10940px 0px; }
+.emoji-1F434 { background-position: -10960px 0px; }
+.emoji-1F435 { background-position: -10980px 0px; }
+.emoji-1F436 { background-position: -11000px 0px; }
+.emoji-1F437 { background-position: -11020px 0px; }
+.emoji-1F438 { background-position: -11040px 0px; }
+.emoji-1F439 { background-position: -11060px 0px; }
+.emoji-1F43A { background-position: -11080px 0px; }
+.emoji-1F43B { background-position: -11100px 0px; }
+.emoji-1F43C { background-position: -11120px 0px; }
+.emoji-1F43D { background-position: -11140px 0px; }
+.emoji-1F43E { background-position: -11160px 0px; }
+.emoji-1F43F { background-position: -11180px 0px; }
+.emoji-1F440 { background-position: -11200px 0px; }
+.emoji-1F441 { background-position: -11220px 0px; }
+.emoji-1F442 { background-position: -11240px 0px; }
+.emoji-1F443 { background-position: -11260px 0px; }
+.emoji-1F444 { background-position: -11280px 0px; }
+.emoji-1F445 { background-position: -11300px 0px; }
+.emoji-1F446 { background-position: -11320px 0px; }
+.emoji-1F447 { background-position: -11340px 0px; }
+.emoji-1F448 { background-position: -11360px 0px; }
+.emoji-1F449 { background-position: -11380px 0px; }
+.emoji-1F44A { background-position: -11400px 0px; }
+.emoji-1F44B { background-position: -11420px 0px; }
+.emoji-1F44C { background-position: -11440px 0px; }
+.emoji-1F44D { background-position: -11460px 0px; }
+.emoji-1F44E { background-position: -11480px 0px; }
+.emoji-1F44F { background-position: -11500px 0px; }
+.emoji-1F450 { background-position: -11520px 0px; }
+.emoji-1F451 { background-position: -11540px 0px; }
+.emoji-1F452 { background-position: -11560px 0px; }
+.emoji-1F453 { background-position: -11580px 0px; }
+.emoji-1F454 { background-position: -11600px 0px; }
+.emoji-1F455 { background-position: -11620px 0px; }
+.emoji-1F456 { background-position: -11640px 0px; }
+.emoji-1F457 { background-position: -11660px 0px; }
+.emoji-1F458 { background-position: -11680px 0px; }
+.emoji-1F459 { background-position: -11700px 0px; }
+.emoji-1F45A { background-position: -11720px 0px; }
+.emoji-1F45B { background-position: -11740px 0px; }
+.emoji-1F45C { background-position: -11760px 0px; }
+.emoji-1F45D { background-position: -11780px 0px; }
+.emoji-1F45E { background-position: -11800px 0px; }
+.emoji-1F45F { background-position: -11820px 0px; }
+.emoji-1F460 { background-position: -11840px 0px; }
+.emoji-1F461 { background-position: -11860px 0px; }
+.emoji-1F462 { background-position: -11880px 0px; }
+.emoji-1F463 { background-position: -11900px 0px; }
+.emoji-1F464 { background-position: -11920px 0px; }
+.emoji-1F465 { background-position: -11940px 0px; }
+.emoji-1F466 { background-position: -11960px 0px; }
+.emoji-1F467 { background-position: -11980px 0px; }
+.emoji-1F468 { background-position: -12000px 0px; }
+.emoji-1F468-1F468-1F466 { background-position: -12020px 0px; }
+.emoji-1F468-1F468-1F466-1F466 { background-position: -12040px 0px; }
+.emoji-1F468-1F468-1F467 { background-position: -12060px 0px; }
+.emoji-1F468-1F468-1F467-1F466 { background-position: -12080px 0px; }
+.emoji-1F468-1F468-1F467-1F467 { background-position: -12100px 0px; }
+.emoji-1F468-1F469-1F466-1F466 { background-position: -12120px 0px; }
+.emoji-1F468-1F469-1F467 { background-position: -12140px 0px; }
+.emoji-1F468-1F469-1F467-1F466 { background-position: -12160px 0px; }
+.emoji-1F468-1F469-1F467-1F467 { background-position: -12180px 0px; }
+.emoji-1F468-2764-1F468 { background-position: -12200px 0px; }
+.emoji-1F468-2764-1F48B-1F468 { background-position: -12220px 0px; }
+.emoji-1F469 { background-position: -12240px 0px; }
+.emoji-1F469-1F469-1F466 { background-position: -12260px 0px; }
+.emoji-1F469-1F469-1F466-1F466 { background-position: -12280px 0px; }
+.emoji-1F469-1F469-1F467 { background-position: -12300px 0px; }
+.emoji-1F469-1F469-1F467-1F466 { background-position: -12320px 0px; }
+.emoji-1F469-1F469-1F467-1F467 { background-position: -12340px 0px; }
+.emoji-1F469-2764-1F469 { background-position: -12360px 0px; }
+.emoji-1F469-2764-1F48B-1F469 { background-position: -12380px 0px; }
+.emoji-1F46A { background-position: -12400px 0px; }
+.emoji-1F46B { background-position: -12420px 0px; }
+.emoji-1F46C { background-position: -12440px 0px; }
+.emoji-1F46D { background-position: -12460px 0px; }
+.emoji-1F46E { background-position: -12480px 0px; }
+.emoji-1F46F { background-position: -12500px 0px; }
+.emoji-1F470 { background-position: -12520px 0px; }
+.emoji-1F471 { background-position: -12540px 0px; }
+.emoji-1F472 { background-position: -12560px 0px; }
+.emoji-1F473 { background-position: -12580px 0px; }
+.emoji-1F474 { background-position: -12600px 0px; }
+.emoji-1F475 { background-position: -12620px 0px; }
+.emoji-1F476 { background-position: -12640px 0px; }
+.emoji-1F477 { background-position: -12660px 0px; }
+.emoji-1F478 { background-position: -12680px 0px; }
+.emoji-1F479 { background-position: -12700px 0px; }
+.emoji-1F47A { background-position: -12720px 0px; }
+.emoji-1F47B { background-position: -12740px 0px; }
+.emoji-1F47C { background-position: -12760px 0px; }
+.emoji-1F47D { background-position: -12780px 0px; }
+.emoji-1F47E { background-position: -12800px 0px; }
+.emoji-1F47F { background-position: -12820px 0px; }
+.emoji-1F480 { background-position: -12840px 0px; }
+.emoji-1F481 { background-position: -12860px 0px; }
+.emoji-1F482 { background-position: -12880px 0px; }
+.emoji-1F483 { background-position: -12900px 0px; }
+.emoji-1F484 { background-position: -12920px 0px; }
+.emoji-1F485 { background-position: -12940px 0px; }
+.emoji-1F486 { background-position: -12960px 0px; }
+.emoji-1F487 { background-position: -12980px 0px; }
+.emoji-1F488 { background-position: -13000px 0px; }
+.emoji-1F489 { background-position: -13020px 0px; }
+.emoji-1F48A { background-position: -13040px 0px; }
+.emoji-1F48B { background-position: -13060px 0px; }
+.emoji-1F48C { background-position: -13080px 0px; }
+.emoji-1F48D { background-position: -13100px 0px; }
+.emoji-1F48E { background-position: -13120px 0px; }
+.emoji-1F48F { background-position: -13140px 0px; }
+.emoji-1F490 { background-position: -13160px 0px; }
+.emoji-1F491 { background-position: -13180px 0px; }
+.emoji-1F492 { background-position: -13200px 0px; }
+.emoji-1F493 { background-position: -13220px 0px; }
+.emoji-1F494 { background-position: -13240px 0px; }
+.emoji-1F495 { background-position: -13260px 0px; }
+.emoji-1F496 { background-position: -13280px 0px; }
+.emoji-1F497 { background-position: -13300px 0px; }
+.emoji-1F498 { background-position: -13320px 0px; }
+.emoji-1F499 { background-position: -13340px 0px; }
+.emoji-1F49A { background-position: -13360px 0px; }
+.emoji-1F49B { background-position: -13380px 0px; }
+.emoji-1F49C { background-position: -13400px 0px; }
+.emoji-1F49D { background-position: -13420px 0px; }
+.emoji-1F49E { background-position: -13440px 0px; }
+.emoji-1F49F { background-position: -13460px 0px; }
+.emoji-1F4A0 { background-position: -13480px 0px; }
+.emoji-1F4A1 { background-position: -13500px 0px; }
+.emoji-1F4A2 { background-position: -13520px 0px; }
+.emoji-1F4A3 { background-position: -13540px 0px; }
+.emoji-1F4A4 { background-position: -13560px 0px; }
+.emoji-1F4A5 { background-position: -13580px 0px; }
+.emoji-1F4A6 { background-position: -13600px 0px; }
+.emoji-1F4A7 { background-position: -13620px 0px; }
+.emoji-1F4A8 { background-position: -13640px 0px; }
+.emoji-1F4A9 { background-position: -13660px 0px; }
+.emoji-1F4AA { background-position: -13680px 0px; }
+.emoji-1F4AB { background-position: -13700px 0px; }
+.emoji-1F4AC { background-position: -13720px 0px; }
+.emoji-1F4AD { background-position: -13740px 0px; }
+.emoji-1F4AE { background-position: -13760px 0px; }
+.emoji-1F4AF { background-position: -13780px 0px; }
+.emoji-1F4B0 { background-position: -13800px 0px; }
+.emoji-1F4B1 { background-position: -13820px 0px; }
+.emoji-1F4B2 { background-position: -13840px 0px; }
+.emoji-1F4B3 { background-position: -13860px 0px; }
+.emoji-1F4B4 { background-position: -13880px 0px; }
+.emoji-1F4B5 { background-position: -13900px 0px; }
+.emoji-1F4B6 { background-position: -13920px 0px; }
+.emoji-1F4B7 { background-position: -13940px 0px; }
+.emoji-1F4B8 { background-position: -13960px 0px; }
+.emoji-1F4B9 { background-position: -13980px 0px; }
+.emoji-1F4BA { background-position: -14000px 0px; }
+.emoji-1F4BB { background-position: -14020px 0px; }
+.emoji-1F4BC { background-position: -14040px 0px; }
+.emoji-1F4BD { background-position: -14060px 0px; }
+.emoji-1F4BE { background-position: -14080px 0px; }
+.emoji-1F4BF { background-position: -14100px 0px; }
+.emoji-1F4C0 { background-position: -14120px 0px; }
+.emoji-1F4C1 { background-position: -14140px 0px; }
+.emoji-1F4C2 { background-position: -14160px 0px; }
+.emoji-1F4C3 { background-position: -14180px 0px; }
+.emoji-1F4C4 { background-position: -14200px 0px; }
+.emoji-1F4C5 { background-position: -14220px 0px; }
+.emoji-1F4C6 { background-position: -14240px 0px; }
+.emoji-1F4C7 { background-position: -14260px 0px; }
+.emoji-1F4C8 { background-position: -14280px 0px; }
+.emoji-1F4C9 { background-position: -14300px 0px; }
+.emoji-1F4CA { background-position: -14320px 0px; }
+.emoji-1F4CB { background-position: -14340px 0px; }
+.emoji-1F4CC { background-position: -14360px 0px; }
+.emoji-1F4CD { background-position: -14380px 0px; }
+.emoji-1F4CE { background-position: -14400px 0px; }
+.emoji-1F4CF { background-position: -14420px 0px; }
+.emoji-1F4D0 { background-position: -14440px 0px; }
+.emoji-1F4D1 { background-position: -14460px 0px; }
+.emoji-1F4D2 { background-position: -14480px 0px; }
+.emoji-1F4D3 { background-position: -14500px 0px; }
+.emoji-1F4D4 { background-position: -14520px 0px; }
+.emoji-1F4D5 { background-position: -14540px 0px; }
+.emoji-1F4D6 { background-position: -14560px 0px; }
+.emoji-1F4D7 { background-position: -14580px 0px; }
+.emoji-1F4D8 { background-position: -14600px 0px; }
+.emoji-1F4D9 { background-position: -14620px 0px; }
+.emoji-1F4DA { background-position: -14640px 0px; }
+.emoji-1F4DB { background-position: -14660px 0px; }
+.emoji-1F4DC { background-position: -14680px 0px; }
+.emoji-1F4DD { background-position: -14700px 0px; }
+.emoji-1F4DE { background-position: -14720px 0px; }
+.emoji-1F4DF { background-position: -14740px 0px; }
+.emoji-1F4E0 { background-position: -14760px 0px; }
+.emoji-1F4E1 { background-position: -14780px 0px; }
+.emoji-1F4E2 { background-position: -14800px 0px; }
+.emoji-1F4E3 { background-position: -14820px 0px; }
+.emoji-1F4E4 { background-position: -14840px 0px; }
+.emoji-1F4E5 { background-position: -14860px 0px; }
+.emoji-1F4E6 { background-position: -14880px 0px; }
+.emoji-1F4E7 { background-position: -14900px 0px; }
+.emoji-1F4E8 { background-position: -14920px 0px; }
+.emoji-1F4E9 { background-position: -14940px 0px; }
+.emoji-1F4EA { background-position: -14960px 0px; }
+.emoji-1F4EB { background-position: -14980px 0px; }
+.emoji-1F4EC { background-position: -15000px 0px; }
+.emoji-1F4ED { background-position: -15020px 0px; }
+.emoji-1F4EE { background-position: -15040px 0px; }
+.emoji-1F4EF { background-position: -15060px 0px; }
+.emoji-1F4F0 { background-position: -15080px 0px; }
+.emoji-1F4F1 { background-position: -15100px 0px; }
+.emoji-1F4F2 { background-position: -15120px 0px; }
+.emoji-1F4F3 { background-position: -15140px 0px; }
+.emoji-1F4F4 { background-position: -15160px 0px; }
+.emoji-1F4F5 { background-position: -15180px 0px; }
+.emoji-1F4F6 { background-position: -15200px 0px; }
+.emoji-1F4F7 { background-position: -15220px 0px; }
+.emoji-1F4F8 { background-position: -15240px 0px; }
+.emoji-1F4F9 { background-position: -15260px 0px; }
+.emoji-1F4FA { background-position: -15280px 0px; }
+.emoji-1F4FB { background-position: -15300px 0px; }
+.emoji-1F4FC { background-position: -15320px 0px; }
+.emoji-1F4FD { background-position: -15340px 0px; }
+.emoji-1F4FE { background-position: -15360px 0px; }
+.emoji-1F500 { background-position: -15380px 0px; }
+.emoji-1F501 { background-position: -15400px 0px; }
+.emoji-1F502 { background-position: -15420px 0px; }
+.emoji-1F503 { background-position: -15440px 0px; }
+.emoji-1F504 { background-position: -15460px 0px; }
+.emoji-1F505 { background-position: -15480px 0px; }
+.emoji-1F506 { background-position: -15500px 0px; }
+.emoji-1F507 { background-position: -15520px 0px; }
+.emoji-1F508 { background-position: -15540px 0px; }
+.emoji-1F509 { background-position: -15560px 0px; }
+.emoji-1F50A { background-position: -15580px 0px; }
+.emoji-1F50B { background-position: -15600px 0px; }
+.emoji-1F50C { background-position: -15620px 0px; }
+.emoji-1F50D { background-position: -15640px 0px; }
+.emoji-1F50E { background-position: -15660px 0px; }
+.emoji-1F50F { background-position: -15680px 0px; }
+.emoji-1F510 { background-position: -15700px 0px; }
+.emoji-1F511 { background-position: -15720px 0px; }
+.emoji-1F512 { background-position: -15740px 0px; }
+.emoji-1F513 { background-position: -15760px 0px; }
+.emoji-1F514 { background-position: -15780px 0px; }
+.emoji-1F515 { background-position: -15800px 0px; }
+.emoji-1F516 { background-position: -15820px 0px; }
+.emoji-1F517 { background-position: -15840px 0px; }
+.emoji-1F518 { background-position: -15860px 0px; }
+.emoji-1F519 { background-position: -15880px 0px; }
+.emoji-1F51A { background-position: -15900px 0px; }
+.emoji-1F51B { background-position: -15920px 0px; }
+.emoji-1F51C { background-position: -15940px 0px; }
+.emoji-1F51D { background-position: -15960px 0px; }
+.emoji-1F51E { background-position: -15980px 0px; }
+.emoji-1F51F { background-position: -16000px 0px; }
+.emoji-1F520 { background-position: -16020px 0px; }
+.emoji-1F521 { background-position: -16040px 0px; }
+.emoji-1F522 { background-position: -16060px 0px; }
+.emoji-1F523 { background-position: -16080px 0px; }
+.emoji-1F524 { background-position: -16100px 0px; }
+.emoji-1F525 { background-position: -16120px 0px; }
+.emoji-1F526 { background-position: -16140px 0px; }
+.emoji-1F527 { background-position: -16160px 0px; }
+.emoji-1F528 { background-position: -16180px 0px; }
+.emoji-1F529 { background-position: -16200px 0px; }
+.emoji-1F52A { background-position: -16220px 0px; }
+.emoji-1F52B { background-position: -16240px 0px; }
+.emoji-1F52C { background-position: -16260px 0px; }
+.emoji-1F52D { background-position: -16280px 0px; }
+.emoji-1F52E { background-position: -16300px 0px; }
+.emoji-1F52F { background-position: -16320px 0px; }
+.emoji-1F530 { background-position: -16340px 0px; }
+.emoji-1F531 { background-position: -16360px 0px; }
+.emoji-1F532 { background-position: -16380px 0px; }
+.emoji-1F533 { background-position: -16400px 0px; }
+.emoji-1F534 { background-position: -16420px 0px; }
+.emoji-1F535 { background-position: -16440px 0px; }
+.emoji-1F536 { background-position: -16460px 0px; }
+.emoji-1F537 { background-position: -16480px 0px; }
+.emoji-1F538 { background-position: -16500px 0px; }
+.emoji-1F539 { background-position: -16520px 0px; }
+.emoji-1F53A { background-position: -16540px 0px; }
+.emoji-1F53B { background-position: -16560px 0px; }
+.emoji-1F53C { background-position: -16580px 0px; }
+.emoji-1F53D { background-position: -16600px 0px; }
+.emoji-1F546 { background-position: -16620px 0px; }
+.emoji-1F547 { background-position: -16640px 0px; }
+.emoji-1F548 { background-position: -16660px 0px; }
+.emoji-1F549 { background-position: -16680px 0px; }
+.emoji-1F54A { background-position: -16700px 0px; }
+.emoji-1F550 { background-position: -16720px 0px; }
+.emoji-1F551 { background-position: -16740px 0px; }
+.emoji-1F552 { background-position: -16760px 0px; }
+.emoji-1F553 { background-position: -16780px 0px; }
+.emoji-1F554 { background-position: -16800px 0px; }
+.emoji-1F555 { background-position: -16820px 0px; }
+.emoji-1F556 { background-position: -16840px 0px; }
+.emoji-1F557 { background-position: -16860px 0px; }
+.emoji-1F558 { background-position: -16880px 0px; }
+.emoji-1F559 { background-position: -16900px 0px; }
+.emoji-1F55A { background-position: -16920px 0px; }
+.emoji-1F55B { background-position: -16940px 0px; }
+.emoji-1F55C { background-position: -16960px 0px; }
+.emoji-1F55D { background-position: -16980px 0px; }
+.emoji-1F55E { background-position: -17000px 0px; }
+.emoji-1F55F { background-position: -17020px 0px; }
+.emoji-1F560 { background-position: -17040px 0px; }
+.emoji-1F561 { background-position: -17060px 0px; }
+.emoji-1F562 { background-position: -17080px 0px; }
+.emoji-1F563 { background-position: -17100px 0px; }
+.emoji-1F564 { background-position: -17120px 0px; }
+.emoji-1F565 { background-position: -17140px 0px; }
+.emoji-1F566 { background-position: -17160px 0px; }
+.emoji-1F567 { background-position: -17180px 0px; }
+.emoji-1F568 { background-position: -17200px 0px; }
+.emoji-1F569 { background-position: -17220px 0px; }
+.emoji-1F56A { background-position: -17240px 0px; }
+.emoji-1F56B { background-position: -17260px 0px; }
+.emoji-1F56C { background-position: -17280px 0px; }
+.emoji-1F56D { background-position: -17300px 0px; }
+.emoji-1F56E { background-position: -17320px 0px; }
+.emoji-1F56F { background-position: -17340px 0px; }
+.emoji-1F570 { background-position: -17360px 0px; }
+.emoji-1F571 { background-position: -17380px 0px; }
+.emoji-1F572 { background-position: -17400px 0px; }
+.emoji-1F573 { background-position: -17420px 0px; }
+.emoji-1F574 { background-position: -17440px 0px; }
+.emoji-1F575 { background-position: -17460px 0px; }
+.emoji-1F576 { background-position: -17480px 0px; }
+.emoji-1F577 { background-position: -17500px 0px; }
+.emoji-1F578 { background-position: -17520px 0px; }
+.emoji-1F579 { background-position: -17540px 0px; }
+.emoji-1F57B { background-position: -17560px 0px; }
+.emoji-1F57E { background-position: -17580px 0px; }
+.emoji-1F57F { background-position: -17600px 0px; }
+.emoji-1F581 { background-position: -17620px 0px; }
+.emoji-1F582 { background-position: -17640px 0px; }
+.emoji-1F583 { background-position: -17660px 0px; }
+.emoji-1F585 { background-position: -17680px 0px; }
+.emoji-1F586 { background-position: -17700px 0px; }
+.emoji-1F587 { background-position: -17720px 0px; }
+.emoji-1F588 { background-position: -17740px 0px; }
+.emoji-1F589 { background-position: -17760px 0px; }
+.emoji-1F58A { background-position: -17780px 0px; }
+.emoji-1F58B { background-position: -17800px 0px; }
+.emoji-1F58C { background-position: -17820px 0px; }
+.emoji-1F58D { background-position: -17840px 0px; }
+.emoji-1F58E { background-position: -17860px 0px; }
+.emoji-1F58F { background-position: -17880px 0px; }
+.emoji-1F590 { background-position: -17900px 0px; }
+.emoji-1F591 { background-position: -17920px 0px; }
+.emoji-1F592 { background-position: -17940px 0px; }
+.emoji-1F593 { background-position: -17960px 0px; }
+.emoji-1F594 { background-position: -17980px 0px; }
+.emoji-1F595 { background-position: -18000px 0px; }
+.emoji-1F596 { background-position: -18020px 0px; }
+.emoji-1F597 { background-position: -18040px 0px; }
+.emoji-1F598 { background-position: -18060px 0px; }
+.emoji-1F599 { background-position: -18080px 0px; }
+.emoji-1F59E { background-position: -18100px 0px; }
+.emoji-1F59F { background-position: -18120px 0px; }
+.emoji-1F5A5 { background-position: -18140px 0px; }
+.emoji-1F5A6 { background-position: -18160px 0px; }
+.emoji-1F5A7 { background-position: -18180px 0px; }
+.emoji-1F5A8 { background-position: -18200px 0px; }
+.emoji-1F5A9 { background-position: -18220px 0px; }
+.emoji-1F5AA { background-position: -18240px 0px; }
+.emoji-1F5AB { background-position: -18260px 0px; }
+.emoji-1F5AD { background-position: -18280px 0px; }
+.emoji-1F5AE { background-position: -18300px 0px; }
+.emoji-1F5AF { background-position: -18320px 0px; }
+.emoji-1F5B2 { background-position: -18340px 0px; }
+.emoji-1F5B3 { background-position: -18360px 0px; }
+.emoji-1F5B4 { background-position: -18380px 0px; }
+.emoji-1F5B8 { background-position: -18400px 0px; }
+.emoji-1F5B9 { background-position: -18420px 0px; }
+.emoji-1F5BC { background-position: -18440px 0px; }
+.emoji-1F5BD { background-position: -18460px 0px; }
+.emoji-1F5BE { background-position: -18480px 0px; }
+.emoji-1F5C0 { background-position: -18500px 0px; }
+.emoji-1F5C1 { background-position: -18520px 0px; }
+.emoji-1F5C2 { background-position: -18540px 0px; }
+.emoji-1F5C3 { background-position: -18560px 0px; }
+.emoji-1F5C4 { background-position: -18580px 0px; }
+.emoji-1F5C6 { background-position: -18600px 0px; }
+.emoji-1F5C7 { background-position: -18620px 0px; }
+.emoji-1F5C9 { background-position: -18640px 0px; }
+.emoji-1F5CA { background-position: -18660px 0px; }
+.emoji-1F5CE { background-position: -18680px 0px; }
+.emoji-1F5CF { background-position: -18700px 0px; }
+.emoji-1F5D0 { background-position: -18720px 0px; }
+.emoji-1F5D1 { background-position: -18740px 0px; }
+.emoji-1F5D2 { background-position: -18760px 0px; }
+.emoji-1F5D3 { background-position: -18780px 0px; }
+.emoji-1F5D4 { background-position: -18800px 0px; }
+.emoji-1F5D8 { background-position: -18820px 0px; }
+.emoji-1F5D9 { background-position: -18840px 0px; }
+.emoji-1F5DC { background-position: -18860px 0px; }
+.emoji-1F5DD { background-position: -18880px 0px; }
+.emoji-1F5DE { background-position: -18900px 0px; }
+.emoji-1F5E0 { background-position: -18920px 0px; }
+.emoji-1F5E1 { background-position: -18940px 0px; }
+.emoji-1F5E2 { background-position: -18960px 0px; }
+.emoji-1F5E3 { background-position: -18980px 0px; }
+.emoji-1F5E8 { background-position: -19000px 0px; }
+.emoji-1F5E9 { background-position: -19020px 0px; }
+.emoji-1F5EA { background-position: -19040px 0px; }
+.emoji-1F5EB { background-position: -19060px 0px; }
+.emoji-1F5EC { background-position: -19080px 0px; }
+.emoji-1F5ED { background-position: -19100px 0px; }
+.emoji-1F5EE { background-position: -19120px 0px; }
+.emoji-1F5EF { background-position: -19140px 0px; }
+.emoji-1F5F0 { background-position: -19160px 0px; }
+.emoji-1F5F1 { background-position: -19180px 0px; }
+.emoji-1F5F2 { background-position: -19200px 0px; }
+.emoji-1F5F3 { background-position: -19220px 0px; }
+.emoji-1F5F4 { background-position: -19240px 0px; }
+.emoji-1F5F5 { background-position: -19260px 0px; }
+.emoji-1F5F8 { background-position: -19280px 0px; }
+.emoji-1F5F9 { background-position: -19300px 0px; }
+.emoji-1F5FA { background-position: -19320px 0px; }
+.emoji-1F5FB { background-position: -19340px 0px; }
+.emoji-1F5FC { background-position: -19360px 0px; }
+.emoji-1F5FD { background-position: -19380px 0px; }
+.emoji-1F5FE { background-position: -19400px 0px; }
+.emoji-1F5FF { background-position: -19420px 0px; }
+.emoji-1F600 { background-position: -19440px 0px; }
+.emoji-1F601 { background-position: -19460px 0px; }
+.emoji-1F602 { background-position: -19480px 0px; }
+.emoji-1F603 { background-position: -19500px 0px; }
+.emoji-1F604 { background-position: -19520px 0px; }
+.emoji-1F605 { background-position: -19540px 0px; }
+.emoji-1F606 { background-position: -19560px 0px; }
+.emoji-1F607 { background-position: -19580px 0px; }
+.emoji-1F608 { background-position: -19600px 0px; }
+.emoji-1F609 { background-position: -19620px 0px; }
+.emoji-1F60A { background-position: -19640px 0px; }
+.emoji-1F60B { background-position: -19660px 0px; }
+.emoji-1F60C { background-position: -19680px 0px; }
+.emoji-1F60D { background-position: -19700px 0px; }
+.emoji-1F60E { background-position: -19720px 0px; }
+.emoji-1F60F { background-position: -19740px 0px; }
+.emoji-1F610 { background-position: -19760px 0px; }
+.emoji-1F611 { background-position: -19780px 0px; }
+.emoji-1F612 { background-position: -19800px 0px; }
+.emoji-1F613 { background-position: -19820px 0px; }
+.emoji-1F614 { background-position: -19840px 0px; }
+.emoji-1F615 { background-position: -19860px 0px; }
+.emoji-1F616 { background-position: -19880px 0px; }
+.emoji-1F617 { background-position: -19900px 0px; }
+.emoji-1F618 { background-position: -19920px 0px; }
+.emoji-1F619 { background-position: -19940px 0px; }
+.emoji-1F61A { background-position: -19960px 0px; }
+.emoji-1F61B { background-position: -19980px 0px; }
+.emoji-1F61C { background-position: -20000px 0px; }
+.emoji-1F61D { background-position: -20020px 0px; }
+.emoji-1F61E { background-position: -20040px 0px; }
+.emoji-1F61F { background-position: -20060px 0px; }
+.emoji-1F620 { background-position: -20080px 0px; }
+.emoji-1F621 { background-position: -20100px 0px; }
+.emoji-1F622 { background-position: -20120px 0px; }
+.emoji-1F623 { background-position: -20140px 0px; }
+.emoji-1F624 { background-position: -20160px 0px; }
+.emoji-1F625 { background-position: -20180px 0px; }
+.emoji-1F626 { background-position: -20200px 0px; }
+.emoji-1F627 { background-position: -20220px 0px; }
+.emoji-1F628 { background-position: -20240px 0px; }
+.emoji-1F629 { background-position: -20260px 0px; }
+.emoji-1F62A { background-position: -20280px 0px; }
+.emoji-1F62B { background-position: -20300px 0px; }
+.emoji-1F62C { background-position: -20320px 0px; }
+.emoji-1F62D { background-position: -20340px 0px; }
+.emoji-1F62E { background-position: -20360px 0px; }
+.emoji-1F62F { background-position: -20380px 0px; }
+.emoji-1F630 { background-position: -20400px 0px; }
+.emoji-1F631 { background-position: -20420px 0px; }
+.emoji-1F632 { background-position: -20440px 0px; }
+.emoji-1F633 { background-position: -20460px 0px; }
+.emoji-1F634 { background-position: -20480px 0px; }
+.emoji-1F635 { background-position: -20500px 0px; }
+.emoji-1F636 { background-position: -20520px 0px; }
+.emoji-1F637 { background-position: -20540px 0px; }
+.emoji-1F638 { background-position: -20560px 0px; }
+.emoji-1F639 { background-position: -20580px 0px; }
+.emoji-1F63A { background-position: -20600px 0px; }
+.emoji-1F63B { background-position: -20620px 0px; }
+.emoji-1F63C { background-position: -20640px 0px; }
+.emoji-1F63D { background-position: -20660px 0px; }
+.emoji-1F63E { background-position: -20680px 0px; }
+.emoji-1F63F { background-position: -20700px 0px; }
+.emoji-1F640 { background-position: -20720px 0px; }
+.emoji-1F641 { background-position: -20740px 0px; }
+.emoji-1F642 { background-position: -20760px 0px; }
+.emoji-1F645 { background-position: -20780px 0px; }
+.emoji-1F646 { background-position: -20800px 0px; }
+.emoji-1F647 { background-position: -20820px 0px; }
+.emoji-1F648 { background-position: -20840px 0px; }
+.emoji-1F649 { background-position: -20860px 0px; }
+.emoji-1F64A { background-position: -20880px 0px; }
+.emoji-1F64B { background-position: -20900px 0px; }
+.emoji-1F64C { background-position: -20920px 0px; }
+.emoji-1F64D { background-position: -20940px 0px; }
+.emoji-1F64E { background-position: -20960px 0px; }
+.emoji-1F64F { background-position: -20980px 0px; }
+.emoji-1F680 { background-position: -21000px 0px; }
+.emoji-1F681 { background-position: -21020px 0px; }
+.emoji-1F682 { background-position: -21040px 0px; }
+.emoji-1F683 { background-position: -21060px 0px; }
+.emoji-1F684 { background-position: -21080px 0px; }
+.emoji-1F685 { background-position: -21100px 0px; }
+.emoji-1F686 { background-position: -21120px 0px; }
+.emoji-1F687 { background-position: -21140px 0px; }
+.emoji-1F688 { background-position: -21160px 0px; }
+.emoji-1F689 { background-position: -21180px 0px; }
+.emoji-1F68A { background-position: -21200px 0px; }
+.emoji-1F68B { background-position: -21220px 0px; }
+.emoji-1F68C { background-position: -21240px 0px; }
+.emoji-1F68D { background-position: -21260px 0px; }
+.emoji-1F68E { background-position: -21280px 0px; }
+.emoji-1F68F { background-position: -21300px 0px; }
+.emoji-1F690 { background-position: -21320px 0px; }
+.emoji-1F691 { background-position: -21340px 0px; }
+.emoji-1F692 { background-position: -21360px 0px; }
+.emoji-1F693 { background-position: -21380px 0px; }
+.emoji-1F694 { background-position: -21400px 0px; }
+.emoji-1F695 { background-position: -21420px 0px; }
+.emoji-1F696 { background-position: -21440px 0px; }
+.emoji-1F697 { background-position: -21460px 0px; }
+.emoji-1F698 { background-position: -21480px 0px; }
+.emoji-1F699 { background-position: -21500px 0px; }
+.emoji-1F69A { background-position: -21520px 0px; }
+.emoji-1F69B { background-position: -21540px 0px; }
+.emoji-1F69C { background-position: -21560px 0px; }
+.emoji-1F69D { background-position: -21580px 0px; }
+.emoji-1F69E { background-position: -21600px 0px; }
+.emoji-1F69F { background-position: -21620px 0px; }
+.emoji-1F6A0 { background-position: -21640px 0px; }
+.emoji-1F6A1 { background-position: -21660px 0px; }
+.emoji-1F6A2 { background-position: -21680px 0px; }
+.emoji-1F6A3 { background-position: -21700px 0px; }
+.emoji-1F6A4 { background-position: -21720px 0px; }
+.emoji-1F6A5 { background-position: -21740px 0px; }
+.emoji-1F6A6 { background-position: -21760px 0px; }
+.emoji-1F6A7 { background-position: -21780px 0px; }
+.emoji-1F6A8 { background-position: -21800px 0px; }
+.emoji-1F6A9 { background-position: -21820px 0px; }
+.emoji-1F6AA { background-position: -21840px 0px; }
+.emoji-1F6AB { background-position: -21860px 0px; }
+.emoji-1F6AC { background-position: -21880px 0px; }
+.emoji-1F6AD { background-position: -21900px 0px; }
+.emoji-1F6AE { background-position: -21920px 0px; }
+.emoji-1F6AF { background-position: -21940px 0px; }
+.emoji-1F6B0 { background-position: -21960px 0px; }
+.emoji-1F6B1 { background-position: -21980px 0px; }
+.emoji-1F6B2 { background-position: -22000px 0px; }
+.emoji-1F6B3 { background-position: -22020px 0px; }
+.emoji-1F6B4 { background-position: -22040px 0px; }
+.emoji-1F6B5 { background-position: -22060px 0px; }
+.emoji-1F6B6 { background-position: -22080px 0px; }
+.emoji-1F6B7 { background-position: -22100px 0px; }
+.emoji-1F6B8 { background-position: -22120px 0px; }
+.emoji-1F6B9 { background-position: -22140px 0px; }
+.emoji-1F6BA { background-position: -22160px 0px; }
+.emoji-1F6BB { background-position: -22180px 0px; }
+.emoji-1F6BC { background-position: -22200px 0px; }
+.emoji-1F6BD { background-position: -22220px 0px; }
+.emoji-1F6BE { background-position: -22240px 0px; }
+.emoji-1F6BF { background-position: -22260px 0px; }
+.emoji-1F6C0 { background-position: -22280px 0px; }
+.emoji-1F6C1 { background-position: -22300px 0px; }
+.emoji-1F6C2 { background-position: -22320px 0px; }
+.emoji-1F6C3 { background-position: -22340px 0px; }
+.emoji-1F6C4 { background-position: -22360px 0px; }
+.emoji-1F6C5 { background-position: -22380px 0px; }
+.emoji-1F6C6 { background-position: -22400px 0px; }
+.emoji-1F6C7 { background-position: -22420px 0px; }
+.emoji-1F6C8 { background-position: -22440px 0px; }
+.emoji-1F6C9 { background-position: -22460px 0px; }
+.emoji-1F6CA { background-position: -22480px 0px; }
+.emoji-1F6CB { background-position: -22500px 0px; }
+.emoji-1F6CC { background-position: -22520px 0px; }
+.emoji-1F6CD { background-position: -22540px 0px; }
+.emoji-1F6CE { background-position: -22560px 0px; }
+.emoji-1F6CF { background-position: -22580px 0px; }
+.emoji-1F6E0 { background-position: -22600px 0px; }
+.emoji-1F6E1 { background-position: -22620px 0px; }
+.emoji-1F6E2 { background-position: -22640px 0px; }
+.emoji-1F6E3 { background-position: -22660px 0px; }
+.emoji-1F6E4 { background-position: -22680px 0px; }
+.emoji-1F6E5 { background-position: -22700px 0px; }
+.emoji-1F6E6 { background-position: -22720px 0px; }
+.emoji-1F6E7 { background-position: -22740px 0px; }
+.emoji-1F6E8 { background-position: -22760px 0px; }
+.emoji-1F6E9 { background-position: -22780px 0px; }
+.emoji-1F6EA { background-position: -22800px 0px; }
+.emoji-1F6EB { background-position: -22820px 0px; }
+.emoji-1F6EC { background-position: -22840px 0px; }
+.emoji-1F6F0 { background-position: -22860px 0px; }
+.emoji-1F6F1 { background-position: -22880px 0px; }
+.emoji-1F6F2 { background-position: -22900px 0px; }
+.emoji-1F6F3 { background-position: -22920px 0px; }
+.emoji-203C { background-position: -22940px 0px; }
+.emoji-2049 { background-position: -22960px 0px; }
+.emoji-2122 { background-position: -22980px 0px; }
+.emoji-2139 { background-position: -23000px 0px; }
+.emoji-2194 { background-position: -23020px 0px; }
+.emoji-2195 { background-position: -23040px 0px; }
+.emoji-2196 { background-position: -23060px 0px; }
+.emoji-2197 { background-position: -23080px 0px; }
+.emoji-2198 { background-position: -23100px 0px; }
+.emoji-2199 { background-position: -23120px 0px; }
+.emoji-21A9 { background-position: -23140px 0px; }
+.emoji-21AA { background-position: -23160px 0px; }
+.emoji-231A { background-position: -23180px 0px; }
+.emoji-231B { background-position: -23200px 0px; }
+.emoji-23E9 { background-position: -23220px 0px; }
+.emoji-23EA { background-position: -23240px 0px; }
+.emoji-23EB { background-position: -23260px 0px; }
+.emoji-23EC { background-position: -23280px 0px; }
+.emoji-23F0 { background-position: -23300px 0px; }
+.emoji-23F3 { background-position: -23320px 0px; }
+.emoji-24C2 { background-position: -23340px 0px; }
+.emoji-25AA { background-position: -23360px 0px; }
+.emoji-25AB { background-position: -23380px 0px; }
+.emoji-25B6 { background-position: -23400px 0px; }
+.emoji-25C0 { background-position: -23420px 0px; }
+.emoji-25FB { background-position: -23440px 0px; }
+.emoji-25FC { background-position: -23460px 0px; }
+.emoji-25FD { background-position: -23480px 0px; }
+.emoji-25FE { background-position: -23500px 0px; }
+.emoji-2600 { background-position: -23520px 0px; }
+.emoji-2601 { background-position: -23540px 0px; }
+.emoji-260E { background-position: -23560px 0px; }
+.emoji-2611 { background-position: -23580px 0px; }
+.emoji-2614 { background-position: -23600px 0px; }
+.emoji-2615 { background-position: -23620px 0px; }
+.emoji-261D { background-position: -23640px 0px; }
+.emoji-263A { background-position: -23660px 0px; }
+.emoji-2648 { background-position: -23680px 0px; }
+.emoji-2649 { background-position: -23700px 0px; }
+.emoji-264A { background-position: -23720px 0px; }
+.emoji-264B { background-position: -23740px 0px; }
+.emoji-264C { background-position: -23760px 0px; }
+.emoji-264D { background-position: -23780px 0px; }
+.emoji-264E { background-position: -23800px 0px; }
+.emoji-264F { background-position: -23820px 0px; }
+.emoji-2650 { background-position: -23840px 0px; }
+.emoji-2651 { background-position: -23860px 0px; }
+.emoji-2652 { background-position: -23880px 0px; }
+.emoji-2653 { background-position: -23900px 0px; }
+.emoji-2660 { background-position: -23920px 0px; }
+.emoji-2663 { background-position: -23940px 0px; }
+.emoji-2665 { background-position: -23960px 0px; }
+.emoji-2666 { background-position: -23980px 0px; }
+.emoji-2668 { background-position: -24000px 0px; }
+.emoji-267B { background-position: -24020px 0px; }
+.emoji-267F { background-position: -24040px 0px; }
+.emoji-2693 { background-position: -24060px 0px; }
+.emoji-26A0 { background-position: -24080px 0px; }
+.emoji-26A1 { background-position: -24100px 0px; }
+.emoji-26AA { background-position: -24120px 0px; }
+.emoji-26AB { background-position: -24140px 0px; }
+.emoji-26BD { background-position: -24160px 0px; }
+.emoji-26BE { background-position: -24180px 0px; }
+.emoji-26C4 { background-position: -24200px 0px; }
+.emoji-26C5 { background-position: -24220px 0px; }
+.emoji-26CE { background-position: -24240px 0px; }
+.emoji-26D4 { background-position: -24260px 0px; }
+.emoji-26EA { background-position: -24280px 0px; }
+.emoji-26F2 { background-position: -24300px 0px; }
+.emoji-26F3 { background-position: -24320px 0px; }
+.emoji-26F5 { background-position: -24340px 0px; }
+.emoji-26FA { background-position: -24360px 0px; }
+.emoji-26FD { background-position: -24380px 0px; }
+.emoji-2702 { background-position: -24400px 0px; }
+.emoji-2705 { background-position: -24420px 0px; }
+.emoji-2708 { background-position: -24440px 0px; }
+.emoji-2709 { background-position: -24460px 0px; }
+.emoji-270A { background-position: -24480px 0px; }
+.emoji-270B { background-position: -24500px 0px; }
+.emoji-270C { background-position: -24520px 0px; }
+.emoji-270F { background-position: -24540px 0px; }
+.emoji-2712 { background-position: -24560px 0px; }
+.emoji-2714 { background-position: -24580px 0px; }
+.emoji-2716 { background-position: -24600px 0px; }
+.emoji-2728 { background-position: -24620px 0px; }
+.emoji-2733 { background-position: -24640px 0px; }
+.emoji-2734 { background-position: -24660px 0px; }
+.emoji-2744 { background-position: -24680px 0px; }
+.emoji-2747 { background-position: -24700px 0px; }
+.emoji-274C { background-position: -24720px 0px; }
+.emoji-274E { background-position: -24740px 0px; }
+.emoji-2753 { background-position: -24760px 0px; }
+.emoji-2754 { background-position: -24780px 0px; }
+.emoji-2755 { background-position: -24800px 0px; }
+.emoji-2757 { background-position: -24820px 0px; }
+.emoji-2764 { background-position: -24840px 0px; }
+.emoji-2795 { background-position: -24860px 0px; }
+.emoji-2796 { background-position: -24880px 0px; }
+.emoji-2797 { background-position: -24900px 0px; }
+.emoji-27A1 { background-position: -24920px 0px; }
+.emoji-27B0 { background-position: -24940px 0px; }
+.emoji-27BF { background-position: -24960px 0px; }
+.emoji-2934 { background-position: -24980px 0px; }
+.emoji-2935 { background-position: -25000px 0px; }
+.emoji-2B05 { background-position: -25020px 0px; }
+.emoji-2B06 { background-position: -25040px 0px; }
+.emoji-2B07 { background-position: -25060px 0px; }
+.emoji-2B1B { background-position: -25080px 0px; }
+.emoji-2B1C { background-position: -25100px 0px; }
+.emoji-2B50 { background-position: -25120px 0px; }
+.emoji-2B55 { background-position: -25140px 0px; }
+.emoji-3030 { background-position: -25160px 0px; }
+.emoji-303D { background-position: -25180px 0px; }
+.emoji-3297 { background-position: -25200px 0px; }
+.emoji-3299 { background-position: -25220px 0px; } \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index dfb901652bf..282aaf2219b 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,7 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding;
+ padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
margin-left: -$gl-padding;
margin-right: -$gl-padding;
border-bottom: 1px solid $table-border-color;
@@ -16,10 +16,7 @@
top: -2px;
}
- .event-title {
- line-height: 44px;
- }
-
+ .event-title,
.event-item-timestamp {
line-height: 44px;
}
@@ -30,7 +27,7 @@
}
.avatar {
- margin-right: 15px;
+ margin-left: -($gl-avatar-size + 15px);
}
.event-title {
@@ -43,8 +40,7 @@
}
.event-body {
- margin-left: 63px;
- margin-right: 80px;
+ margin-right: 174px;
.event-note {
margin-top: 5px;
@@ -155,6 +151,8 @@
@media (max-width: $screen-xs-max) {
.event-item {
+ padding-left: $gl-padding;
+
.event-title {
white-space: normal;
overflow: visible;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 07a38a19fad..263993f59a5 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,8 +1,3 @@
-.new-group-member-holder {
- margin-top: 50px;
- padding-top: 20px;
-}
-
.member-search-form {
float: left;
}
@@ -15,4 +10,4 @@
.form-control {
height: 42px;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9da085a3473..9da273a0b6b 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -24,20 +24,6 @@
}
}
-.issuable-context-title {
- margin-bottom: 5px;
-
- .avatar {
- margin-left: 0;
- }
-
- label {
- color: $gl-gray;
- font-weight: normal;
- margin-right: 4px;
- }
-}
-
.project-issuable-filter {
.controls {
float: right;
@@ -50,33 +36,82 @@
}
.issuable-details {
- .page-title {
- margin-top: -15px;
- padding: 10px 0;
- margin-bottom: 0;
- color: #5c5d5e;
- font-size: 16px;
-
- .author {
- color: #5c5d5e;
+ section {
+ border-right: 1px solid $border-white-light;
+
+ .issuable-discussion {
+ margin-right: 1px;
}
+ }
+}
+
+.issuable-filter-count {
+ span {
+ display: block;
+ margin-bottom: -16px;
+ padding: 13px 0;
+ }
+}
- .issue-id {
- color: #5c5d5e;
+.issuable-show-labels {
+ a {
+ margin-right: 5px;
+ margin-bottom: 5px;
+ display: inline-block;
+ .color-label {
+ padding: 6px 10px;
}
}
+}
+
+.issuable-sidebar {
+ .block {
+ @include clearfix;
+ padding: $gl-padding 0;
+ border-bottom: 1px solid #F0F0F0;
- .issue-title {
- margin: 0;
- font-size: 23px;
- color: #313236;
+ &:last-child {
+ border: none;
+ }
}
- .description {
- margin-top: 6px;
+ .title {
+ color: $gl-text-color;
+ margin-bottom: 8px;
- p:last-child {
- margin-bottom: 0;
+ .avatar {
+ margin-left: 0;
}
+
+ label {
+ font-weight: normal;
+ margin-right: 4px;
+ }
+
+ .edit-link {
+ color: $gl-gray;
+ }
+ }
+
+ .cross-project-reference {
+ font-weight: bold;
+ color: $gl-link-color;
+
+ button {
+ float: right;
+ }
+ }
+
+ .selectbox {
+ display: none
+ }
+
+ .btn-clipboard {
+ color: $gl-gray;
+ }
+
+ .participants .avatar {
+ margin-top: 6px;
+ margin-right: 2px;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 41c069f0ad3..a02a3a72e79 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -56,21 +56,30 @@
}
}
-.issue-show-labels {
- a {
- margin-right: 5px;
- margin-bottom: 5px;
- display: inline-block;
- .color-label {
- padding: 6px 10px;
- }
- }
-}
-
form.edit-issue {
margin: 0;
}
+.merge-requests-title {
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.merge-request-id {
+ display: inline-block;
+ width: 3em;
+}
+
+.merge-request-info {
+ padding-left: 5px;
+}
+
+.merge-request-status {
+ color: $gl-gray;
+ font-size: 15px;
+ font-weight: bold;
+}
+
.merge-request,
.issue {
&.today {
@@ -132,11 +141,6 @@ form.edit-issue {
}
}
-.issue-closed-by-widget {
- padding: 16px 0;
- margin: 0px;
-}
-
.issue-form .select2-container {
width: 250px !important;
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 83b866c3a64..f9c6f1b39f9 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -19,6 +19,7 @@
h1:first-child {
font-weight: normal;
margin-bottom: 30px;
+ margin-top: 0;
}
img {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index a1a5208c59c..82effde0bf3 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -4,7 +4,6 @@
*/
.mr-state-widget {
background: #F7F8FA;
- margin-bottom: 20px;
color: $gl-gray;
border: 1px solid #dce0e6;
@include border-radius(2px);
@@ -19,18 +18,34 @@
.accept-merge-holder {
.accept-action {
display: inline-block;
+ float: left;
+
+ .accept_merge_request {
+ &.ci-pending,
+ &.ci-running {
+ @include btn-orange;
+ }
+
+ &.ci-skipped,
+ &.ci-failed,
+ &.ci-canceled,
+ &.ci-error {
+ @include btn-red;
+ }
+ }
}
.accept-control {
display: inline-block;
+ float: left;
margin: 0;
margin-left: 20px;
padding: 5px;
+ padding-top: 12px;
line-height: 20px;
&.right {
float: right;
- padding-top: 12px;
a {
color: $gl-gray;
}
@@ -68,12 +83,16 @@
&.ci-error {
color: $gl-danger;
}
+
+ a.monospace {
+ color: inherit;
+ }
}
.mr-widget-body,
.ci_widget,
.mr-widget-footer {
- padding: 15px;
+ padding: $gl-padding;
}
.normal {
@@ -102,28 +121,6 @@
}
}
-.merge-request .merge-request-tabs {
- @include nav-menu;
- margin: -$gl-padding;
- padding: $gl-padding;
- text-align: center;
- margin-bottom: 1px;
-}
-
-// Mobile
-@media (max-width: 480px) {
- .merge-request .merge-request-tabs {
- margin: 0;
- padding: 0;
-
- li {
- a {
- padding: 0;
- }
- }
- }
-}
-
.mr_source_commit,
.mr_target_commit {
.commit {
@@ -141,7 +138,7 @@
font-family: $monospace_font;
font-weight: bold;
overflow: hidden;
- font-size: 14px;
+ font-size: 90%;
margin: 0 3px;
}
@@ -178,33 +175,27 @@
line-height: 1.1;
}
-.merge-request-form-info {
- padding-top: 15px;
-}
-
// hide mr close link for inline diff comment form
.diff-file .close-mr-link,
.diff-file .reopen-mr-link {
display: none;
}
-.merge-request-show-labels {
- a {
- margin-right: 5px;
- margin-bottom: 5px;
- display: inline-block;
- .color-label {
- padding: 6px 10px;
- }
- }
-}
-
.merge-request-form .select2-container {
width: 250px !important;
}
#modal_merge_info .modal-dialog {
width: 600px;
+
+ .btn-clipboard {
+ @extend .pull-right;
+
+ margin-right: 20px;
+ margin-top: 5px;
+ position: absolute;
+ right: 0;
+ }
}
.mr-source-target {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 4392f08942b..d86259f93fb 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -7,6 +7,7 @@
}
.reply-btn {
@extend .btn-primary;
+ margin: 10px $gl-padding;
}
.diff-file .diff-content {
tr.line_holder:hover {
@@ -38,9 +39,8 @@
}
.new_note, .edit_note {
- .buttons {
- margin-top: 8px;
- margin-bottom: 3px;
+ .note-form-actions {
+ margin-top: $gl-padding;
}
.note-preview-holder {
@@ -56,6 +56,10 @@
.note_text {
width: 100%;
}
+
+ .comment-hints {
+ margin-top: -12px;
+ }
}
/* loading indicator */
@@ -71,17 +75,15 @@
.common-note-form {
margin: 0;
- background: #F7F8FA;
+ background: #fff;
padding: $gl-padding;
margin-left: -$gl-padding;
margin-right: -$gl-padding;
- border-right: 1px solid #ECEEF1;
- border-top: 1px solid #ECEEF1;
margin-bottom: -$gl-padding;
}
.note-form-actions {
- background: #F9F9F9;
+ background: #fff;
.note-form-option {
margin-top: 8px;
@@ -146,7 +148,6 @@
.discussion-reply-holder {
background: $background-color;
- padding: 10px 15px;
border-top: 1px solid $border-color;
}
}
@@ -168,7 +169,7 @@
color: #999;
background: #FFF;
padding: 7px;
- margin-top: -11px;
+ margin-top: -7px;
border: 1px solid $border-color;
font-size: 13px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 1980fe0d458..72b0ed29a69 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -109,13 +109,9 @@ ul.notes {
}
}
- // Reduce left padding of first task list ul element
- ul.task-list:first-child {
- padding-left: 10px;
-
- // sub-tasks should be padded normally
- ul {
- padding-left: 20px;
+ ul.task-list {
+ ul:not(.task-list) {
+ padding-left: 1.3em;
}
}
@@ -132,7 +128,7 @@ ul.notes {
}
&:last-child {
- border-bottom: none;
+ border-bottom: 1px solid $border-color;
}
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b7391e5303b..95fc26a608a 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -5,12 +5,6 @@
}
}
-.btn-build-token {
- float: left;
- padding: 6px 20px;
- margin-right: 12px;
-}
-
.profile-avatar-form-option {
hr {
margin: 10px 0;
@@ -53,3 +47,25 @@
float: right;
font-size: 12px;
}
+
+.profile-link-holder {
+ display: inline;
+
+ &:after {
+ content: "\00B7";
+ padding: 0px 6px;
+ font-weight: bold;
+ }
+
+ &:last-child {
+ &:after {
+ content: "";
+ padding: 0;
+ }
+ }
+
+ a {
+ color: $blue-dark;
+ text-decoration: none;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 41bea0ec5c8..be6ef43e49c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -5,7 +5,7 @@
font-weight: normal;
}
}
-.no-ssh-key-message {
+.no-ssh-key-message, .project-limit-message {
background-color: #f28d35;
margin-bottom: 16px;
}
@@ -18,10 +18,6 @@
}
}
-.project-edit-content {
- padding: 7px;
-}
-
.project-name-holder {
.help-inline {
vertical-align: top;
@@ -30,11 +26,12 @@
}
.project-home-panel {
- text-align: center;
- background: #f7f8fa;
- margin: -$gl-padding;
- padding: $gl-padding;
- padding: 44px 0 17px 0;
+
+ .cover-controls {
+ .project-settings-dropdown {
+ margin-left: 10px;
+ }
+ }
.project-identicon-holder {
margin-bottom: 16px;
@@ -50,7 +47,17 @@
}
.project-home-dropdown {
- margin: 11px 3px 0;
+ margin: 13px 0px 0;
+ }
+
+ .notifications-btn {
+ .fa-bell {
+ margin-right: 6px;
+ }
+
+ .fa-angle-down {
+ margin-left: 6px;
+ }
}
.project-home-desc {
@@ -80,27 +87,94 @@
}
.visibility-level-label {
+ @extend .btn;
+ @extend .btn-gray;
+
color: $gray;
+ cursor: default;
+
i {
color: inherit;
}
}
- .input-group {
+
+ .git-clone-holder {
display: inline-table;
position: relative;
- top: 17px;
- margin-bottom: 44px;
}
.project-repo-buttons {
margin-top: 12px;
margin-bottom: 0px;
+ .count-buttons {
+ display: block;
+ margin-bottom: 12px;
+ }
+
.btn {
@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;
+ }
+ }
.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: 20px;
+ padding: 11px 16px;
+ letter-spacing: .4px;
+ padding: 10px;
+ text-align: center;
+ vertical-align: middle;
+ touch-action: manipulation;
+ cursor: pointer;
+ background-image: none;
+ white-space: nowrap;
+ margin: 0 11px 0px 4px;
+
+ &:hover {
+ background: #FFF;
+ }
}
}
}
@@ -120,6 +194,13 @@
margin-right: 45px;
}
+ .clone-options {
+ display: table-cell;
+ a.btn {
+ width: 100%;
+ }
+ }
+
.form-control {
cursor: auto;
@extend .monospace;
@@ -167,6 +248,11 @@
&:active {
outline: none;
}
+
+ &.btn-clipboard {
+ padding-left: 15px;
+ padding-right: 15px;
+ }
}
.active {
@@ -233,23 +319,11 @@
}
}
- .fa-fw {
+ i {
margin-right: 8px;
}
}
-.fa-bell {
- margin-right: 6px;
-}
-
-.fa-angle-down {
- margin-left: 6px;
-}
-
-.project-home-panel .project-home-dropdown {
- margin: 13px 0px 0;
-}
-
.project-visibility-level-holder {
.radio {
margin-bottom: 10px;
@@ -337,6 +411,38 @@ ul.nav.nav-projects-tabs {
}
}
+.top-area {
+ border-bottom: 1px solid #EEE;
+ margin: 0 -16px;
+ padding: 0 $gl-padding;
+
+ ul.left-top-menu {
+ display: inline-block;
+ width: 50%;
+ margin-bottom: 0px;
+ border-bottom: none;
+ }
+
+ .projects-search-form {
+ width: 50%;
+ display: inline-block;
+ float: right;
+ padding-top: 7px;
+ text-align: right;
+
+ .btn-green {
+ margin-top: -2px;
+ margin-left: 10px;
+ }
+ }
+
+ @media (max-width: $screen-xs-max) {
+ .projects-search-form {
+ padding-top: 15px;
+ }
+ }
+}
+
.fork-namespaces {
.fork-thumbnail {
text-align: center;
@@ -367,7 +473,7 @@ table.table.protected-branches-list tr.no-border {
.project-stats {
text-align: center;
- margin-top: 15px;
+ margin-top: $gl-padding;
margin-bottom: 0;
padding-top: 10px;
padding-bottom: 4px;
@@ -414,11 +520,18 @@ pre.light-well {
.projects-search-form {
margin: -$gl-padding;
- background-color: #f8fafc;
padding: $gl-padding;
margin-bottom: 0px;
- border-top: 1px solid #e7e9ed;
- border-bottom: 1px solid #e7e9ed;
+
+ input {
+ display: inline-block;
+ width: calc(100% - 151px);
+ }
+
+ .btn {
+ display: inline-block;
+ width: 135px;
+ }
}
.git-empty {
@@ -544,5 +657,13 @@ pre.light-well {
}
.project-show-readme .readme-holder {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ padding: ($gl-padding + 7px);
border-top: 0;
+
+ .edit-project-readme {
+ z-index: 100;
+ position: relative;
+ }
}
diff --git a/app/assets/stylesheets/pages/runners.scss b/app/assets/stylesheets/pages/runners.scss
index 2b15ab83129..a9111a7388f 100644
--- a/app/assets/stylesheets/pages/runners.scss
+++ b/app/assets/stylesheets/pages/runners.scss
@@ -1,36 +1,34 @@
-.ci-body {
- .runner-state {
- padding: 6px 12px;
- margin-right: 10px;
- color: #FFF;
+.runner-state {
+ padding: 6px 12px;
+ margin-right: 10px;
+ color: #FFF;
- &.runner-state-shared {
- background: #32b186;
- }
- &.runner-state-specific {
- background: #3498db;
- }
+ &.runner-state-shared {
+ background: #32b186;
}
-
- .runner-status-online {
- color: green;
+ &.runner-state-specific {
+ background: #3498db;
}
+}
- .runner-status-offline {
- color: gray;
- }
+.runner-status-online {
+ color: green;
+}
- .runner-status-paused {
- color: red;
- }
+.runner-status-offline {
+ color: gray;
+}
+
+.runner-status-paused {
+ color: red;
+}
- .runner {
- .btn {
- padding: 1px 6px;
- }
+.runner {
+ .btn {
+ padding: 1px 6px;
+ }
- h4 {
- font-weight: normal;
- }
+ h4 {
+ font-weight: normal;
}
}
diff --git a/app/assets/stylesheets/pages/sherlock.scss b/app/assets/stylesheets/pages/sherlock.scss
new file mode 100644
index 00000000000..92d84d9640f
--- /dev/null
+++ b/app/assets/stylesheets/pages/sherlock.scss
@@ -0,0 +1,33 @@
+table .sherlock-code {
+ max-width: 700px;
+}
+
+.sherlock-code {
+ pre {
+ word-wrap: normal;
+ }
+
+ pre code {
+ white-space: pre;
+ }
+}
+
+.sherlock-line-samples-table {
+ margin-bottom: 0px !important;
+
+ thead tr th,
+ tbody tr td {
+ font-size: 13px !important;
+ text-align: right;
+ padding: 0px 10px !important;
+ }
+}
+
+.sherlock-file-sample pre {
+ padding-top: 28px !important;
+}
+
+.sherlock-line-samples-table .slow {
+ color: $red-light;
+ font-weight: bold;
+}
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index a3d7aba054d..1430d01859d 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -1,8 +1,3 @@
-.my-snippets li:first-child {
- h4 { margin-top: 0; }
- padding-top: 0;
-}
-
.snippet-form-holder .file-holder .file-title {
padding: 2px;
}
@@ -30,3 +25,30 @@
}
}
}
+
+.snippet-holder {
+ margin-bottom: -$gl-padding;
+
+ .file-holder {
+ border-top: 0;
+ }
+
+ .file-actions {
+ .btn-clipboard {
+ @extend .btn;
+ }
+ }
+}
+
+.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: 40px;
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index a7d3b2197f1..4b6ef035673 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -35,3 +35,20 @@
border-color: $gl-warning;
}
}
+
+.ci-status-icon-success {
+ @extend .cgreen;
+}
+.ci-status-icon-failed {
+ @extend .cred;
+}
+.ci-status-icon-running,
+.ci-status-icon-pending {
+ // These are standard text color
+}
+.ci-status-icon-canceled,
+.ci-status-icon-disabled,
+.ci-status-icon-not-found,
+.ci-status-icon-skipped {
+ @extend .cgray;
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 1b0cef481d6..d4ab6967ccd 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -5,7 +5,7 @@
tr {
> td, > th {
- line-height: 32px;
+ line-height: 28px;
}
&:hover {
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 277afa1db9e..185f3622e64 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -1,9 +1,6 @@
.gitlab-ui-dev-kit {
> h2 {
- font-size: 27px;
- border-bottom: 1px solid #CCC;
- color: #666;
- margin: 30px 0;
+ margin: 35px 0 20px;
font-weight: bold;
}
}
diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss
index dfaeba41cf6..cdf514197cb 100644
--- a/app/assets/stylesheets/pages/wiki.scss
+++ b/app/assets/stylesheets/pages/wiki.scss
@@ -4,3 +4,8 @@
margin-right: auto;
padding-right: 7px;
}
+
+.wiki-last-edit-by {
+ font-size: 80%;
+ font-weight: normal;
+}
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 2f4054eaa11..38814459f66 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -9,12 +9,10 @@ class AbuseReportsController < ApplicationController
@abuse_report.reporter = current_user
if @abuse_report.save
- if current_application_settings.admin_notification_email.present?
- AbuseReportMailer.delay.notify(@abuse_report.id)
- end
+ @abuse_report.notify
message = "Thank you for your report. A GitLab administrator will look into it shortly."
- redirect_to root_path, notice: message
+ redirect_to @abuse_report.user, notice: message
else
render :new
end
@@ -23,6 +21,9 @@ class AbuseReportsController < ApplicationController
private
def report_params
- params.require(:abuse_report).permit(:user_id, :message)
+ params.require(:abuse_report).permit(%i(
+ message
+ user_id
+ ))
end
end
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index 56e24386463..9083bfb41cf 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -8,4 +8,10 @@ class Admin::ApplicationController < ApplicationController
def authenticate_admin!
return render_404 unless current_user.is_admin?
end
+
+ def authorize_impersonator!
+ if session[:impersonator_id]
+ User.find_by!(username: session[:impersonator_id]).admin?
+ end
+ end
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 039f18f23e0..10e736fd362 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -13,6 +13,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
end
+ def reset_runners_token
+ @application_setting.reset_runners_registration_token!
+ flash[:notice] = 'New runners registration token has been generated!'
+ redirect_to admin_runners_path
+ end
+
private
def set_application_setting
@@ -43,6 +49,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:default_branch_protection,
:signup_enabled,
:signin_enabled,
+ :require_two_factor_authentication,
+ :two_factor_grace_period,
:gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text,
@@ -57,6 +65,19 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:version_check_enabled,
:admin_notification_email,
:user_oauth_applications,
+ :shared_runners_enabled,
+ :max_artifacts_size,
+ :metrics_enabled,
+ :metrics_host,
+ :metrics_port,
+ :metrics_username,
+ :metrics_password,
+ :metrics_pool_size,
+ :metrics_timeout,
+ :metrics_method_call_threshold,
+ :recaptcha_enabled,
+ :recaptcha_site_key,
+ :recaptcha_private_key,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb
new file mode 100644
index 00000000000..0db91eaaf2e
--- /dev/null
+++ b/app/controllers/admin/builds_controller.rb
@@ -0,0 +1,23 @@
+class Admin::BuildsController < Admin::ApplicationController
+ def index
+ @scope = params[:scope]
+ @all_builds = Ci::Build
+ @builds = @all_builds.order('created_at DESC')
+ @builds =
+ case @scope
+ when 'running'
+ @builds.running_or_pending.reverse_order
+ when 'finished'
+ @builds.finished
+ else
+ @builds
+ end
+ @builds = @builds.page(params[:page]).per(30)
+ end
+
+ def cancel_all
+ Ci::Build.running_or_pending.each(&:cancel)
+
+ redirect_to admin_builds_path
+ end
+end
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index d28614731f9..e383fe38ea6 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -1,6 +1,21 @@
class Admin::IdentitiesController < Admin::ApplicationController
before_action :user
- before_action :identity, except: :index
+ before_action :identity, except: [:index, :new, :create]
+
+ def new
+ @identity = Identity.new
+ end
+
+ def create
+ @identity = Identity.new(identity_params)
+ @identity.user_id = user.id
+
+ if @identity.save
+ redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully created.'
+ else
+ render :new
+ end
+ end
def index
@identities = @user.identities
diff --git a/app/controllers/admin/impersonation_controller.rb b/app/controllers/admin/impersonation_controller.rb
new file mode 100644
index 00000000000..bf98af78615
--- /dev/null
+++ b/app/controllers/admin/impersonation_controller.rb
@@ -0,0 +1,38 @@
+class Admin::ImpersonationController < Admin::ApplicationController
+ skip_before_action :authenticate_admin!, only: :destroy
+
+ before_action :user
+ before_action :authorize_impersonator!
+
+ def create
+ if @user.blocked?
+ flash[:alert] = "You cannot impersonate a blocked user"
+
+ redirect_to admin_user_path(@user)
+ else
+ session[:impersonator_id] = current_user.username
+ session[:impersonator_return_to] = admin_user_path(@user)
+
+ warden.set_user(user, scope: 'user')
+
+ flash[:alert] = "You are impersonating #{user.username}."
+
+ redirect_to root_path
+ end
+ end
+
+ def destroy
+ redirect = session[:impersonator_return_to]
+
+ warden.set_user(user, scope: 'user')
+
+ session[:impersonator_return_to] = nil
+ session[:impersonator_id] = nil
+
+ redirect_to redirect || root_path
+ end
+
+ def user
+ @user ||= User.find_by!(username: params[:id] || session[:impersonator_id])
+ end
+end
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
new file mode 100644
index 00000000000..d25619d94e0
--- /dev/null
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -0,0 +1,35 @@
+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)
+ redirect_to admin_runner_path(@runner)
+ else
+ redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ rp = Ci::RunnerProject.find(params[:id])
+ runner = rp.runner
+ rp.destroy
+
+ redirect_to admin_runner_path(runner)
+ end
+
+ private
+
+ def project
+ @project = Project.find_with_namespace(
+ [params[:namespace_id], '/', params[:project_id]].join('')
+ )
+ @project || render_404
+ end
+end
diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb
new file mode 100644
index 00000000000..a701d49b844
--- /dev/null
+++ b/app/controllers/admin/runners_controller.rb
@@ -0,0 +1,63 @@
+class Admin::RunnersController < Admin::ApplicationController
+ before_action :runner, except: :index
+
+ def index
+ @runners = Ci::Runner.order('id DESC')
+ @runners = @runners.search(params[:search]) if params[:search].present?
+ @runners = @runners.page(params[:page]).per(30)
+ @active_runners_cnt = Ci::Runner.online.count
+ 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)
+ end
+
+ def update
+ @runner.update_attributes(runner_params)
+
+ respond_to do |format|
+ format.js
+ format.html { redirect_to admin_runner_path(@runner) }
+ end
+ end
+
+ def destroy
+ @runner.destroy
+
+ redirect_to admin_runners_path
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
+ else
+ redirect_to admin_runners_path, alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
+ else
+ redirect_to admin_runners_path, alert: 'Runner was not updated.'
+ end
+ end
+
+ private
+
+ def runner
+ @runner ||= Ci::Runner.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:token, :description, :tag_list, :active)
+ end
+end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index c63d0793e31..d7c927d444c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -63,12 +63,6 @@ class Admin::UsersController < Admin::ApplicationController
end
end
- def login_as
- sign_in(user)
- flash[:alert] = "Logged in as #{user.username}"
- redirect_to root_path
- end
-
def disable_two_factor
user.disable_two_factor!
redirect_to admin_user_path(user),
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 865deb7d46a..d9a37a4d45f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -10,8 +10,10 @@ class ApplicationController < ActionController::Base
before_action :authenticate_user_from_token!
before_action :authenticate_user!
+ before_action :validate_user_service_ticket!
before_action :reject_blocked!
before_action :check_password_expiration
+ before_action :check_2fa_requirement
before_action :ldap_security_check
before_action :default_headers
before_action :add_gon_variables
@@ -59,13 +61,8 @@ class ApplicationController < ActionController::Base
end
def authenticate_user!(*args)
- # 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
- if current_application_settings.home_page_url.present? &&
- current_application_settings.home_page_url.chomp('/') != Gitlab.config.gitlab['url'].chomp('/')
- if current_user.nil? && root_path == request.path
- redirect_to current_application_settings.home_page_url and return
- end
+ if redirect_to_home_page_url?
+ redirect_to current_application_settings.home_page_url and return
end
super(*args)
@@ -124,7 +121,6 @@ class ApplicationController < ActionController::Base
project_path = "#{namespace}/#{id}"
@project = Project.find_with_namespace(project_path)
-
if @project and can?(current_user, :read_project, @project)
if @project.path_with_namespace != project_path
redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
@@ -208,12 +204,32 @@ class ApplicationController < ActionController::Base
end
end
+ def validate_user_service_ticket!
+ return unless signed_in? && session[:service_tickets]
+
+ valid = session[:service_tickets].all? do |provider, ticket|
+ Gitlab::OAuth::Session.valid?(provider, ticket)
+ end
+
+ unless valid
+ session[:service_tickets] = nil
+ sign_out current_user
+ redirect_to new_user_session_path
+ end
+ end
+
def check_password_expiration
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
+ end
+ end
+
def ldap_security_check
if current_user && current_user.requires_ldap_check?
unless Gitlab::LDAP::Access.allowed?(current_user)
@@ -347,4 +363,34 @@ class ApplicationController < ActionController::Base
def git_import_enabled?
current_application_settings.import_sources.include?('git')
end
+
+ def two_factor_authentication_required?
+ current_application_settings.require_two_factor_authentication
+ end
+
+ def two_factor_grace_period
+ current_application_settings.two_factor_grace_period
+ end
+
+ def two_factor_grace_period_expired?
+ date = current_user.otp_grace_period_started_at
+ date && (date + two_factor_grace_period.hours) < Time.current
+ end
+
+ def skip_two_factor?
+ session[:skip_tfa] && session[:skip_tfa] > Time.current
+ 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
+ return false unless current_application_settings.home_page_url.present?
+
+ home_page_url = current_application_settings.home_page_url.chomp('/')
+ root_urls = [Gitlab.config.gitlab['url'].chomp('/'), root_url.chomp('/')]
+
+ return false if root_urls.include?(home_page_url)
+
+ current_user.nil? && root_path == request.path
+ end
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 202e9da9eee..77c8dafc012 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,41 +1,15 @@
class AutocompleteController < ApplicationController
skip_before_action :authenticate_user!, only: [:users]
+ before_action :find_users, only: [:users]
def users
- begin
- @users =
- if params[:project_id].present?
- project = Project.find(params[:project_id])
-
- if can?(current_user, :read_project, project)
- project.team.users
- end
- elsif params[:group_id]
- group = Group.find(params[:group_id])
-
- if can?(current_user, :read_group, group)
- group.users
- end
- elsif current_user
- User.all
- end
- rescue ActiveRecord::RecordNotFound
- if current_user
- return render json: {}, status: 404
- end
- end
-
- if @users.nil? && current_user.nil?
- authenticate_user!
- end
-
@users ||= User.none
@users = @users.search(params[:search]) if params[:search].present?
@users = @users.active
@users = @users.reorder(:name)
@users = @users.page(params[:page]).per(PER_PAGE)
- unless params[:search].present?
+ if params[:search].blank?
# Include current user if available to filter by "Me"
if params[:current_user] && current_user
@users = [*@users, current_user].uniq
@@ -49,4 +23,25 @@ class AutocompleteController < ApplicationController
@user = User.find(params[:id])
render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
end
+
+ private
+
+ def find_users
+ @users =
+ if params[:project_id].present?
+ project = Project.find(params[:project_id])
+ return render_404 unless can?(current_user, :read_project, project)
+
+ project.team.users
+ elsif params[:group_id].present?
+ group = Group.find(params[:group_id])
+ return render_404 unless can?(current_user, :read_group, group)
+
+ group.users
+ elsif current_user
+ User.all
+ else
+ User.none
+ end
+ end
end
diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb
deleted file mode 100644
index 4ec2dc9c2cf..00000000000
--- a/app/controllers/ci/admin/application_controller.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module Ci
- module Admin
- class ApplicationController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :authenticate_admin!
-
- layout "ci/admin"
- end
- end
-end
diff --git a/app/controllers/ci/admin/application_settings_controller.rb b/app/controllers/ci/admin/application_settings_controller.rb
deleted file mode 100644
index 71e253fac67..00000000000
--- a/app/controllers/ci/admin/application_settings_controller.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Ci
- class Admin::ApplicationSettingsController < Ci::Admin::ApplicationController
- before_action :set_application_setting
-
- def show
- end
-
- def update
- if @application_setting.update_attributes(application_setting_params)
- redirect_to ci_admin_application_settings_path,
- notice: 'Application settings saved successfully'
- else
- render :show
- end
- end
-
- private
-
- def set_application_setting
- @application_setting = Ci::ApplicationSetting.current
- @application_setting ||= Ci::ApplicationSetting.create_from_defaults
- end
-
- def application_setting_params
- params.require(:application_setting).permit(
- :all_broken_builds,
- :add_pusher,
- )
- end
- end
-end
diff --git a/app/controllers/ci/admin/builds_controller.rb b/app/controllers/ci/admin/builds_controller.rb
deleted file mode 100644
index 38abfdeafbf..00000000000
--- a/app/controllers/ci/admin/builds_controller.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module Ci
- class Admin::BuildsController < Ci::Admin::ApplicationController
- def index
- @scope = params[:scope]
- @builds = Ci::Build.order('created_at DESC').page(params[:page]).per(30)
-
- @builds =
- case @scope
- when "pending"
- @builds.pending
- when "running"
- @builds.running
- else
- @builds
- end
- end
- end
-end
diff --git a/app/controllers/ci/admin/events_controller.rb b/app/controllers/ci/admin/events_controller.rb
deleted file mode 100644
index 5939efff980..00000000000
--- a/app/controllers/ci/admin/events_controller.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Ci
- class Admin::EventsController < Ci::Admin::ApplicationController
- EVENTS_PER_PAGE = 50
-
- def index
- @events = Ci::Event.admin.order('created_at DESC').page(params[:page]).per(EVENTS_PER_PAGE)
- end
- end
-end
diff --git a/app/controllers/ci/admin/projects_controller.rb b/app/controllers/ci/admin/projects_controller.rb
deleted file mode 100644
index 5bbd0ce7396..00000000000
--- a/app/controllers/ci/admin/projects_controller.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
- class Admin::ProjectsController < Ci::Admin::ApplicationController
- def index
- @projects = Ci::Project.ordered_by_last_commit_date.page(params[:page]).per(30)
- end
-
- def destroy
- project.destroy
-
- redirect_to ci_projects_url
- end
-
- protected
-
- def project
- @project ||= Ci::Project.find(params[:id])
- end
- end
-end
diff --git a/app/controllers/ci/admin/runner_projects_controller.rb b/app/controllers/ci/admin/runner_projects_controller.rb
deleted file mode 100644
index e7de6eb12ca..00000000000
--- a/app/controllers/ci/admin/runner_projects_controller.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Ci
- class Admin::RunnerProjectsController < Ci::Admin::ApplicationController
- layout 'ci/project'
-
- 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)
- redirect_to ci_admin_runner_path(@runner)
- else
- redirect_to ci_admin_runner_path(@runner), alert: 'Failed adding runner to project'
- end
- end
-
- def destroy
- rp = Ci::RunnerProject.find(params[:id])
- runner = rp.runner
- rp.destroy
-
- redirect_to ci_admin_runner_path(runner)
- end
-
- private
-
- def project
- @project ||= Ci::Project.find(params[:project_id])
- end
- end
-end
diff --git a/app/controllers/ci/admin/runners_controller.rb b/app/controllers/ci/admin/runners_controller.rb
deleted file mode 100644
index 110954a612d..00000000000
--- a/app/controllers/ci/admin/runners_controller.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-module Ci
- class Admin::RunnersController < Ci::Admin::ApplicationController
- before_action :runner, except: :index
-
- def index
- @runners = Ci::Runner.order('id DESC')
- @runners = @runners.search(params[:search]) if params[:search].present?
- @runners = @runners.page(params[:page]).per(30)
- @active_runners_cnt = Ci::Runner.online.count
- end
-
- def show
- @builds = @runner.builds.order('id DESC').first(30)
- @projects = Ci::Project.all
- if params[:search].present?
- @gl_projects = ::Project.search(params[:search])
- @projects = @projects.where(gitlab_id: @gl_projects.select(:id))
- end
- @projects = @projects.where("ci_projects.id NOT IN (?)", @runner.projects.pluck(:id)) if @runner.projects.any?
- @projects = @projects.page(params[:page]).per(30)
- end
-
- def update
- @runner.update_attributes(runner_params)
-
- respond_to do |format|
- format.js
- format.html { redirect_to ci_admin_runner_path(@runner) }
- end
- end
-
- def destroy
- @runner.destroy
-
- redirect_to ci_admin_runners_path
- end
-
- def resume
- if @runner.update_attributes(active: true)
- redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
- else
- redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
- end
- end
-
- def pause
- if @runner.update_attributes(active: false)
- redirect_to ci_admin_runners_path, notice: 'Runner was successfully updated.'
- else
- redirect_to ci_admin_runners_path, alert: 'Runner was not updated.'
- end
- end
-
- def assign_all
- Ci::Project.unassigned(@runner).all.each do |project|
- @runner.assign_to(project, current_user)
- end
-
- redirect_to ci_admin_runner_path(@runner), notice: "Runner was assigned to all projects"
- end
-
- private
-
- def runner
- @runner ||= Ci::Runner.find(params[:id])
- end
-
- def runner_params
- params.require(:runner).permit(:token, :description, :tag_list, :active)
- end
- end
-end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
index 9be470660e6..c420b59c3a2 100644
--- a/app/controllers/ci/application_controller.rb
+++ b/app/controllers/ci/application_controller.rb
@@ -4,32 +4,16 @@ module Ci
"app/helpers/ci"
end
- helper_method :gl_project
-
private
- def authenticate_public_page!
- unless project.public
- authenticate_user!
-
- return access_denied! unless can?(current_user, :read_project, gl_project)
- end
- end
-
- def authenticate_token!
- unless project.valid_token?(params[:token])
- return head(403)
- end
- end
-
def authorize_access_project!
- unless can?(current_user, :read_project, gl_project)
+ unless can?(current_user, :read_project, project)
return page_404
end
end
def authorize_manage_builds!
- unless can?(current_user, :manage_builds, gl_project)
+ unless can?(current_user, :manage_builds, project)
return page_404
end
end
@@ -39,7 +23,7 @@ module Ci
end
def authorize_manage_project!
- unless can?(current_user, :admin_project, gl_project)
+ unless can?(current_user, :admin_project, project)
return page_404
end
end
@@ -66,9 +50,5 @@ module Ci
count: count
}
end
-
- def gl_project
- ::Project.find(@project.gitlab_id)
- end
end
end
diff --git a/app/controllers/ci/events_controller.rb b/app/controllers/ci/events_controller.rb
deleted file mode 100644
index 89b784a1e89..00000000000
--- a/app/controllers/ci/events_controller.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-module Ci
- class EventsController < Ci::ApplicationController
- EVENTS_PER_PAGE = 50
-
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def index
- @events = project.events.order("created_at DESC").page(params[:page]).per(EVENTS_PER_PAGE)
- end
-
- private
-
- def project
- @project ||= Ci::Project.find(params[:project_id])
- end
- end
-end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index 24dd1b5c93a..e782a51e7eb 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -1,5 +1,5 @@
module Ci
- class LintsController < Ci::ApplicationController
+ class LintsController < ApplicationController
before_action :authenticate_user!
def show
@@ -15,12 +15,14 @@ module Ci
@builds = @config_processor.builds
@status = true
end
- rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
@error = e.message
@status = false
- rescue Exception
- @error = "Undefined error"
+ rescue
+ @error = 'Undefined error'
@status = false
+ ensure
+ render :show
end
end
end
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index 809b44387ba..3004c2d27f0 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -3,13 +3,12 @@ module Ci
before_action :project, except: [:index]
before_action :authenticate_user!, except: [:index, :build, :badge]
before_action :authorize_access_project!, except: [:index, :badge]
- before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml]
before_action :no_cache, only: [:badge]
protect_from_forgery
def show
# Temporary compatibility with CI badges pointing to CI project page
- redirect_to namespace_project_path(project.gl_project.namespace, project.gl_project)
+ redirect_to namespace_project_path(project.namespace, project)
end
# Project status badge
@@ -20,20 +19,10 @@ module Ci
send_file image.path, filename: image.name, disposition: 'inline', type:"image/svg+xml"
end
- def toggle_shared_runners
- project.toggle!(:shared_runners_enabled)
-
- redirect_to namespace_project_runners_path(project.gl_project.namespace, project.gl_project)
- end
-
- def dumped_yaml
- send_data @project.generated_yaml_config, filename: '.gitlab-ci.yml'
- end
-
protected
def project
- @project ||= Ci::Project.find(params[:id])
+ @project ||= Project.find_by(ci_id: params[:id].to_i)
end
def no_cache
diff --git a/app/controllers/ci/runner_projects_controller.rb b/app/controllers/ci/runner_projects_controller.rb
deleted file mode 100644
index 97f01d40af5..00000000000
--- a/app/controllers/ci/runner_projects_controller.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- class RunnerProjectsController < Ci::ApplicationController
- before_action :authenticate_user!
- before_action :project
- before_action :authorize_manage_project!
-
- layout 'ci/project'
-
- def create
- @runner = Ci::Runner.find(params[:runner_project][:runner_id])
-
- return head(403) unless current_user.ci_authorized_runners.include?(@runner)
-
- path = runners_path(@project.gl_project)
-
- if @runner.assign_to(project, current_user)
- redirect_to path
- else
- redirect_to path, alert: 'Failed adding runner to project'
- end
- end
-
- def destroy
- runner_project = project.runner_projects.find(params[:id])
- runner_project.destroy
-
- redirect_to runners_path(@project.gl_project)
- end
-
- private
-
- def project
- @project ||= Ci::Project.find(params[:project_id])
- end
- end
-end
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
new file mode 100644
index 00000000000..62127a09081
--- /dev/null
+++ b/app/controllers/concerns/creates_commit.rb
@@ -0,0 +1,103 @@
+module CreatesCommit
+ extend ActiveSupport::Concern
+
+ def create_commit(service, success_path:, failure_path:, failure_view: nil, success_notice: nil)
+ set_commit_variables
+
+ commit_params = @commit_params.merge(
+ source_project: @project,
+ source_branch: @ref,
+ target_branch: @target_branch
+ )
+
+ result = service.new(@tree_edit_project, current_user, commit_params).execute
+
+ if result[:status] == :success
+ flash[:notice] = success_notice || "Your changes have been successfully committed."
+
+ if create_merge_request?
+ success_path = new_merge_request_path
+ target = different_project? ? "project" : "branch"
+ flash[:notice] << " You can now submit a merge request to get this change into the original #{target}."
+ end
+
+ respond_to do |format|
+ format.html { redirect_to success_path }
+ format.json { render json: { message: "success", filePath: success_path } }
+ end
+ else
+ flash[:alert] = result[:message]
+ respond_to do |format|
+ format.html do
+ if failure_view
+ render failure_view
+ else
+ redirect_to failure_path
+ end
+ end
+ format.json { render json: { message: "failed", filePath: failure_path } }
+ end
+ end
+ end
+
+ def authorize_edit_tree!
+ return if can?(current_user, :push_code, project)
+ return if current_user && current_user.already_forked?(project)
+
+ access_denied!
+ end
+
+ private
+
+ def new_merge_request_path
+ new_namespace_project_merge_request_path(
+ @mr_source_project.namespace,
+ @mr_source_project,
+ merge_request: {
+ source_project_id: @mr_source_project.id,
+ target_project_id: @mr_target_project.id,
+ source_branch: @mr_source_branch,
+ target_branch: @mr_target_branch
+ }
+ )
+ end
+
+ def different_project?
+ @mr_source_project != @mr_target_project
+ end
+
+ def different_branch?
+ @mr_source_branch != @mr_target_branch || different_project?
+ end
+
+ def create_merge_request?
+ params[:create_merge_request].present? && different_branch?
+ end
+
+ def set_commit_variables
+ @mr_source_branch = @target_branch
+
+ if can?(current_user, :push_code, @project)
+ # Edit file in this project
+ @tree_edit_project = @project
+ @mr_source_project = @project
+
+ if @project.forked?
+ # Merge request from this project to fork origin
+ @mr_target_project = @project.forked_from_project
+ @mr_target_branch = @mr_target_project.repository.root_ref
+ else
+ # Merge request to this project
+ @mr_target_project = @project
+ @mr_target_branch = @ref
+ end
+ else
+ # Edit file in fork
+ @tree_edit_project = current_user.fork_of(@project)
+ # Merge request from fork to this project
+ @mr_source_project = @tree_edit_project
+ @mr_target_project = @project
+ @mr_target_branch = @mr_target_project.repository.root_ref
+ end
+ end
+end
diff --git a/app/controllers/concerns/global_milestones.rb b/app/controllers/concerns/global_milestones.rb
new file mode 100644
index 00000000000..3e4c0e63601
--- /dev/null
+++ b/app/controllers/concerns/global_milestones.rb
@@ -0,0 +1,21 @@
+module GlobalMilestones
+ extend ActiveSupport::Concern
+
+ def milestones
+ epoch = DateTime.parse('1970-01-01')
+ @milestones = MilestonesFinder.new.execute(@projects, params)
+ @milestones = GlobalMilestone.build_collection(@milestones)
+ @milestones = @milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
+ @milestones = Kaminari.paginate_array(@milestones).page(params[:page]).per(ApplicationController::PER_PAGE)
+ end
+
+ def milestone
+ milestones = Milestone.of_projects(@projects).where(title: params[:title])
+
+ if milestones.present?
+ @milestone = GlobalMilestone.new(params[:title], milestones)
+ else
+ render_404
+ end
+ end
+end
diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb
new file mode 100644
index 00000000000..effd4721949
--- /dev/null
+++ b/app/controllers/concerns/issues_action.rb
@@ -0,0 +1,14 @@
+module IssuesAction
+ extend ActiveSupport::Concern
+
+ def issues
+ @issues = get_issues_collection
+ @issues = @issues.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @issues = @issues.preload(:author, :project)
+
+ respond_to do |format|
+ format.html
+ format.atom { render layout: false }
+ end
+ end
+end
diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb
new file mode 100644
index 00000000000..f7a25111db9
--- /dev/null
+++ b/app/controllers/concerns/merge_requests_action.rb
@@ -0,0 +1,9 @@
+module MergeRequestsAction
+ extend ActiveSupport::Concern
+
+ def merge_requests
+ @merge_requests = get_merge_requests_collection
+ @merge_requests = @merge_requests.page(params[:page]).per(ApplicationController::PER_PAGE)
+ @merge_requests = @merge_requests.preload(:author, :target_project)
+ end
+end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 53896d4f2c7..2bdce0f8a00 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,34 +1,19 @@
class Dashboard::MilestonesController < Dashboard::ApplicationController
- before_action :load_projects
+ include GlobalMilestones
+
+ before_action :projects
+ before_action :milestones, only: [:index]
+ before_action :milestone, only: [:show]
def index
- project_milestones = case params[:state]
- when 'all'; state
- when 'closed'; state('closed')
- else state('active')
- end
- @dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
- @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE)
end
def show
- project_milestones = Milestone.where(project_id: @projects).order("due_date ASC")
- @dashboard_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
end
private
- def load_projects
- @projects = current_user.authorized_projects.sorted_by_activity.non_archived
- end
-
- def title
- params[:title]
- end
-
- def state(state = nil)
- conditions = { project_id: @projects }
- conditions.reverse_merge!(state: state) if state
- Milestone.where(conditions).order("title ASC")
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
end
end
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index f4354c6d8ca..b3594d82530 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -1,6 +1,7 @@
class Dashboard::SnippetsController < Dashboard::ApplicationController
def index
- @snippets = SnippetsFinder.new.execute(current_user,
+ @snippets = SnippetsFinder.new.execute(
+ current_user,
filter: :by_user,
user: current_user,
scope: params[:scope]
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 4ebb3d7276e..087da935087 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,25 +1,12 @@
class DashboardController < Dashboard::ApplicationController
+ include IssuesAction
+ include MergeRequestsAction
+
before_action :event_filter, only: :activity
+ before_action :projects, only: [:issues, :merge_requests]
respond_to :html
- def merge_requests
- @merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
- @merge_requests = @merge_requests.preload(:author, :target_project)
- end
-
- def issues
- @issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(PER_PAGE)
- @issues = @issues.preload(:author, :project)
-
- respond_to do |format|
- format.html
- format.atom { render layout: false }
- end
- end
-
def activity
@last_push = current_user.recent_push
@@ -47,4 +34,8 @@ class DashboardController < Dashboard::ApplicationController
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
+
+ def projects
+ @projects ||= current_user.authorized_projects.sorted_by_activity.non_archived
+ end
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 9575a87ee41..a9bf4321f73 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,6 +1,6 @@
class Explore::GroupsController < Explore::ApplicationController
def index
- @groups = GroupsFinder.new.execute(current_user)
+ @groups = Group.order_id_desc
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.page(params[:page]).per(PER_PAGE)
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 6878d4bc07e..be801858eaf 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -1,8 +1,13 @@
class Groups::ApplicationController < ApplicationController
layout 'group'
+ before_action :group
private
-
+
+ def group
+ @group ||= Group.find_by(path: params[:group_id])
+ end
+
def authorize_read_group!
unless @group and can?(current_user, :read_group, @group)
if current_user.nil?
@@ -12,13 +17,13 @@ class Groups::ApplicationController < ApplicationController
end
end
end
-
+
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
end
end
-
+
def authorize_admin_group_member!
unless can?(current_user, :admin_group_member, group)
return render_403
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 6aa64222f77..76c87366baa 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -1,8 +1,6 @@
-class Groups::AvatarsController < ApplicationController
+class Groups::AvatarsController < Groups::ApplicationController
def destroy
- @group = Group.find_by(path: params[:group_id])
@group.remove_avatar!
-
@group.save
redirect_to edit_group_path(@group)
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 91518c44a98..0e902c4bb43 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,11 +1,9 @@
class Groups::GroupMembersController < Groups::ApplicationController
skip_before_action :authenticate_user!, only: [:index]
- before_action :group
# Authorize
before_action :authorize_read_group!
- before_action :authorize_admin_group!, except: [:index, :leave]
- before_action :authorize_admin_group_member!, only: [:create, :resend_invite]
+ before_action :authorize_admin_group_member!, except: [:index, :leave]
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@@ -18,7 +16,8 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
- @group_member = GroupMember.new
+
+ @group_member = @group.group_members.new
end
def create
@@ -28,24 +27,23 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def update
- @member = @group.group_members.find(params[:id])
+ @group_member = @group.group_members.find(params[:id])
- return render_403 unless can?(current_user, :update_group_member, @member)
+ return render_403 unless can?(current_user, :update_group_member, @group_member)
- @member.update_attributes(member_params)
+ @group_member.update_attributes(member_params)
end
def destroy
@group_member = @group.group_members.find(params[:id])
- if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
- @group_member.destroy
- 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 }
- end
- else
- return render_403
+ return render_403 unless can?(current_user, :destroy_group_member, @group_member)
+
+ @group_member.destroy
+
+ 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 }
end
end
@@ -64,10 +62,11 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def leave
- @group_member = @group.group_members.where(user_id: current_user.id).first
+ @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)
@@ -80,10 +79,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
- def group
- @group ||= Group.find_by(path: params[:group_id])
- end
-
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 669f7f3126d..0c2a350bc39 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,54 +1,55 @@
class Groups::MilestonesController < Groups::ApplicationController
- before_action :authorize_group_milestone!, only: :update
+ include GlobalMilestones
+
+ before_action :projects
+ before_action :milestones, only: [:index]
+ before_action :milestone, only: [:show, :update]
+ before_action :authorize_group_milestone!, only: [:create, :update]
def index
- project_milestones = case params[:state]
- when 'all'; state
- when 'closed'; state('closed')
- else state('active')
- end
- @group_milestones = Milestones::GroupService.new(project_milestones).execute
- @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE)
end
- def show
- project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
- @group_milestone = Milestones::GroupService.new(project_milestones).milestone(title)
+ def new
+ @milestone = Milestone.new
end
- def update
- project_milestones = Milestone.where(project_id: group.projects).order("due_date ASC")
- @group_milestones = Milestones::GroupService.new(project_milestones).milestone(title)
+ def create
+ project_ids = params[:milestone][:project_ids]
+ title = milestone_params[:title]
- @group_milestones.milestones.each do |milestone|
- Milestones::UpdateService.new(milestone.project, current_user, params[:milestone]).execute(milestone)
+ @group.projects.where(id: project_ids).each do |project|
+ Milestones::CreateService.new(project, current_user, milestone_params).execute
end
- respond_to do |format|
- format.js
- format.html do
- redirect_to group_milestones_path(group)
- end
+ redirect_to milestone_path(title)
+ end
+
+ def show
+ end
+
+ def update
+ @milestone.milestones.each do |milestone|
+ Milestones::UpdateService.new(milestone.project, current_user, milestone_params).execute(milestone)
end
+
+ redirect_back_or_default(default: milestone_path(@milestone.title))
end
private
- def group
- @group ||= Group.find_by(path: params[:group_id])
+ def authorize_group_milestone!
+ return render_404 unless can?(current_user, :admin_milestones, group)
end
- def title
- params[:title]
+ def milestone_params
+ params.require(:milestone).permit(:title, :description, :due_date, :state_event)
end
- def state(state = nil)
- conditions = { project_id: group.projects }
- conditions.reverse_merge!(state: state) if state
- Milestone.where(conditions).order("title ASC")
+ def milestone_path(title)
+ group_milestone_path(@group, title.to_slug.to_s, title: title)
end
- def authorize_group_milestone!
- return render_404 unless can?(current_user, :admin_group, group)
+ def projects
+ @projects ||= @group.projects
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 40fb15a5b36..fb26a4e6fc3 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,15 +1,18 @@
class GroupsController < Groups::ApplicationController
+ include IssuesAction
+ include MergeRequestsAction
+
skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests]
respond_to :html
before_action :group, except: [:new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:show, :new, :create]
+ before_action :authorize_read_group!, except: [:show, :new, :create, :autocomplete]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_action :load_projects, except: [:new, :create, :projects, :edit, :update]
+ before_action :load_projects, except: [:new, :create, :projects, :edit, :update, :autocomplete]
before_action :event_filter, only: :show
layout :determine_layout
@@ -53,23 +56,6 @@ class GroupsController < Groups::ApplicationController
end
end
- def merge_requests
- @merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
- @merge_requests = @merge_requests.preload(:author, :target_project)
- end
-
- def issues
- @issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(PER_PAGE)
- @issues = @issues.preload(:author, :project)
-
- respond_to do |format|
- format.html
- format.atom { render layout: false }
- end
- end
-
def edit
end
@@ -133,7 +119,7 @@ class GroupsController < Groups::ApplicationController
end
def group_params
- params.require(:group).permit(:name, :description, :path, :avatar)
+ params.require(:group).permit(:name, :description, :path, :avatar, :public)
end
def load_events
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index f809fa7500a..4cad98b8e98 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -1,6 +1,6 @@
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
- protect_from_forgery except: [:kerberos, :saml]
+ protect_from_forgery except: [:kerberos, :saml, :cas3]
Gitlab.config.omniauth.providers.each do |provider|
define_method provider['name'] do
@@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
render 'errors/omniauth_error', layout: "errors", status: 422
end
+ def cas3
+ ticket = params['ticket']
+ if ticket
+ handle_service_ticket oauth['provider'], ticket
+ end
+ handle_omniauth
+ end
+
private
def handle_omniauth
@@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to new_user_session_path
end
+ def handle_service_ticket provider, ticket
+ Gitlab::OAuth::Session.create provider, ticket
+ session[:service_tickets] ||= {}
+ session[:service_tickets][provider] = ticket
+ end
+
def oauth
@oauth ||= request.env['omniauth.auth']
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 2025158d065..f74daff3bd0 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -40,7 +40,9 @@ class PasswordsController < Devise::PasswordsController
def throttle_reset
return unless resource && resource.recently_sent_password_reset?
- redirect_to new_password_path(resource_name),
- alert: I18n.t('devise.passwords.recently_reset')
+ # Throttle reset attempts, but return a normal message to
+ # avoid user enumeration attack.
+ redirect_to new_user_session_path,
+ notice: I18n.t('devise.passwords.send_paranoid_instructions')
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index e6b99be37fb..6e91d9b4ad9 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -1,8 +1,22 @@
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
+ skip_before_action :check_2fa_requirement
+
def new
unless current_user.otp_secret
current_user.otp_secret = User.generate_otp_secret(32)
- current_user.save!
+ end
+
+ unless current_user.otp_grace_period_started_at && two_factor_grace_period
+ current_user.otp_grace_period_started_at = Time.current
+ end
+
+ current_user.save! if current_user.changed?
+
+ if two_factor_grace_period_expired?
+ flash.now[:alert] = 'You must configure Two-Factor Authentication in your account.'
+ else
+ grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
+ flash.now[:alert] = "You must configure Two-Factor Authentication in your account until #{l(grace_period_deadline)}."
end
@qr_code = build_qr_code
@@ -34,6 +48,15 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
redirect_to profile_account_path
end
+ def skip
+ if two_factor_grace_period_expired?
+ redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
+ else
+ session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
+ redirect_to root_path
+ end
+ end
+
private
def build_qr_code
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 8da7b4d50ea..28803164fcf 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -70,6 +70,7 @@ class ProfilesController < Profiles::ApplicationController
:email,
:hide_no_password,
:hide_no_ssh_key,
+ :hide_project_limit,
:linkedin,
:location,
:name,
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 519d6d6127e..dd32d509191 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -21,18 +21,14 @@ class Projects::ApplicationController < ApplicationController
unless @repository.branch_names.include?(@ref)
redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref),
- notice: "This action is not allowed unless you are on top of a branch"
+ notice: "This action is not allowed unless you are on a branch"
)
end
end
private
- def ci_enabled
- return render_404 unless @project.gitlab_ci?
- end
-
- def ci_project
- @ci_project ||= @project.ensure_gitlab_ci_project
+ def builds_enabled
+ return render_404 unless @project.builds_enabled?
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 8cc2f21d887..c56a3497bb2 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -1,6 +1,7 @@
# Controller for viewing a file's blame
class Projects::BlobController < Projects::ApplicationController
include ExtractsPath
+ include CreatesCommit
include ActionView::Helpers::SanitizeHelper
# Raised when given an invalid file path
@@ -8,35 +9,23 @@ class Projects::BlobController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:destroy, :create]
+ before_action :authorize_edit_tree!, only: [:new, :create, :edit, :update, :destroy]
before_action :assign_blob_vars
before_action :commit, except: [:new, :create]
before_action :blob, except: [:new, :create]
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 :after_edit_path, only: [:edit, :update]
def new
commit unless @repository.empty?
end
def create
- result = Files::CreateService.new(@project, current_user, @commit_params).execute
-
- if result[:status] == :success
- flash[:notice] = "The changes have been successfully committed"
- respond_to do |format|
- format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }
- format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } }
- end
- else
- flash[:alert] = result[:message]
- respond_to do |format|
- format.html { render :new }
- format.json { render json: { message: "failed", filePath: namespace_project_blob_path(@project.namespace, @project, @id) } }
- end
- end
+ create_commit(Files::CreateService, success_notice: "The file has been successfully created.",
+ success_path: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)),
+ failure_view: :new,
+ failure_path: namespace_project_new_blob_path(@project.namespace, @project, @ref))
end
def show
@@ -47,21 +36,17 @@ class Projects::BlobController < Projects::ApplicationController
end
def update
- result = Files::UpdateService.new(@project, current_user, @commit_params).execute
-
- if result[:status] == :success
- flash[:notice] = "Your changes have been successfully committed"
- respond_to do |format|
- format.html { redirect_to after_edit_path }
- format.json { render json: { message: "success", filePath: after_edit_path } }
- end
- else
- flash[:alert] = result[:message]
- respond_to do |format|
- format.html { render :edit }
- format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
+ after_edit_path =
+ if from_merge_request && @target_branch == @ref
+ diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
+ "#file-path-#{hexdigest(@path)}"
+ else
+ namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
end
- end
+
+ create_commit(Files::UpdateService, success_path: after_edit_path,
+ failure_view: :edit,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def preview
@@ -73,15 +58,10 @@ class Projects::BlobController < Projects::ApplicationController
end
def destroy
- result = Files::DeleteService.new(@project, current_user, @commit_params).execute
-
- if result[:status] == :success
- flash[:notice] = "Your changes have been successfully committed"
- redirect_to namespace_project_tree_path(@project.namespace, @project, @target_branch)
- else
- flash[:alert] = result[:message]
- render :show
- end
+ create_commit(Files::DeleteService, success_notice: "The file has been successfully deleted.",
+ success_path: namespace_project_tree_path(@project.namespace, @project, @target_branch),
+ failure_view: :show,
+ failure_path: namespace_project_blob_path(@project.namespace, @project, @id))
end
def diff
@@ -131,37 +111,20 @@ class Projects::BlobController < Projects::ApplicationController
render_404
end
- def after_edit_path
- @after_edit_path ||=
- if from_merge_request
- diffs_namespace_project_merge_request_path(from_merge_request.target_project.namespace, from_merge_request.target_project, from_merge_request) +
- "#file-path-#{hexdigest(@path)}"
- elsif @target_branch.present?
- namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @path))
- else
- namespace_project_blob_path(@project.namespace, @project, @id)
- end
- end
-
def from_merge_request
# If blob edit was initiated from merge request page
@from_merge_request ||= MergeRequest.find_by(id: params[:from_merge_request_id])
end
- def sanitized_new_branch_name
- @new_branch ||= sanitize(strip_tags(params[:new_branch]))
- end
-
def editor_variables
- @current_branch = @ref
- @target_branch = params[:new_branch].present? ? sanitized_new_branch_name : @ref
+ @target_branch = params[:target_branch]
@file_path =
if action_name.to_s == 'create'
if params[:file].present?
params[:file_name] = params[:file].original_filename
end
- File.join(@path, File.basename(params[:file_name]))
+ File.join(@path, params[:file_name])
else
@path
end
@@ -173,8 +136,6 @@ class Projects::BlobController < Projects::ApplicationController
@commit_params = {
file_path: @file_path,
- current_branch: @current_branch,
- target_branch: @target_branch,
commit_message: params[:commit_message],
file_content: params[:content],
file_content_encoding: params[:encoding]
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 3ac0a75fa70..4db3b3bf23d 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -3,12 +3,17 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:create, :destroy]
+ before_action :authorize_push_code!, only: [:new, :create, :destroy]
def index
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
+
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
end
def recent
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 816012762ce..39d3ba26ba2 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,35 +1,36 @@
class Projects::BuildsController < Projects::ApplicationController
- before_action :ci_project
before_action :build, except: [:index, :cancel_all]
- before_action :authorize_admin_project!, except: [:index, :show, :status]
+ before_action :authorize_manage_builds!, except: [:index, :show, :status]
+ before_action :authorize_download_build_artifacts!, only: [:download]
layout "project"
def index
@scope = params[:scope]
- @all_builds = project.ci_builds
+ @all_builds = project.builds
+ @builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @all_builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
- @all_builds.finished
+ @builds.finished
else
- @all_builds.running_or_pending
+ @builds
end
- @builds = @builds.order('created_at DESC').page(params[:page]).per(30)
+ @builds = @builds.page(params[:page]).per(30)
end
def cancel_all
- @project.ci_builds.running_or_pending.each(&:cancel)
+ @project.builds.running_or_pending.each(&:cancel)
redirect_to namespace_project_builds_path(project.namespace, project)
end
def show
- @builds = @ci_project.commits.find_by_sha(@build.sha).builds.order('id DESC')
- @builds = @builds.where("id not in (?)", @build.id).page(params[:page]).per(20)
+ @builds = @project.ci_commits.find_by_sha(@build.sha).builds.order('id DESC')
+ @builds = @builds.where("id not in (?)", @build.id)
@commit = @build.commit
respond_to do |format|
@@ -41,17 +42,25 @@ class Projects::BuildsController < Projects::ApplicationController
end
def retry
- if @build.commands.blank?
+ unless @build.retryable?
return page_404
end
build = Ci::Build.retry(@build)
- if params[:return_to]
- redirect_to URI.parse(params[:return_to]).path
- else
- redirect_to build_path(build)
+ redirect_to build_path(build)
+ end
+
+ def download
+ unless artifacts_file.file_storage?
+ return redirect_to artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ return not_found!
end
+
+ send_file artifacts_file.path, disposition: 'attachment'
end
def status
@@ -67,10 +76,30 @@ class Projects::BuildsController < Projects::ApplicationController
private
def build
- @build ||= ci_project.builds.unscoped.find_by!(id: params[:id])
+ @build ||= project.builds.unscoped.find_by!(id: params[:id])
+ end
+
+ def artifacts_file
+ build.artifacts_file
end
def build_path(build)
- namespace_project_build_path(build.gl_project.namespace, build.gl_project, build)
+ namespace_project_build_path(build.project.namespace, build.project, build)
+ end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, project)
+ return page_404
+ end
+ end
+
+ def authorize_download_build_artifacts!
+ unless can?(current_user, :download_build_artifacts, @project)
+ if current_user.nil?
+ return authenticate_user!
+ else
+ return render_404
+ end
+ end
end
end
diff --git a/app/controllers/projects/ci_services_controller.rb b/app/controllers/projects/ci_services_controller.rb
deleted file mode 100644
index 406f313ae79..00000000000
--- a/app/controllers/projects/ci_services_controller.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-class Projects::CiServicesController < Projects::ApplicationController
- before_action :ci_project
- before_action :authorize_admin_project!
-
- layout "project_settings"
-
- def index
- @ci_project.build_missing_services
- @services = @ci_project.services.reload
- end
-
- def edit
- service
- end
-
- def update
- if @service.update_attributes(service_params)
- redirect_to edit_namespace_project_ci_service_path(@project, @project.namespace, @service.to_param)
- else
- render 'edit'
- end
- end
-
- def test
- last_build = @project.builds.last
-
- if @service.execute(last_build)
- message = { notice: 'We successfully tested the service' }
- else
- message = { alert: 'We tried to test the service but error occurred' }
- end
-
- redirect_back_or_default(options: message)
- end
-
- private
-
- def service
- @service ||= @ci_project.services.find { |service| service.to_param == params[:id] }
- end
-
- def service_params
- params.require(:service).permit(
- :type, :active, :webhook, :notify_only_broken_builds,
- :email_recipients, :email_only_broken_builds, :email_add_pusher,
- :hipchat_token, :hipchat_room, :hipchat_server
- )
- end
-end
diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb
deleted file mode 100644
index a263242a850..00000000000
--- a/app/controllers/projects/ci_settings_controller.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-class Projects::CiSettingsController < Projects::ApplicationController
- before_action :ci_project
- before_action :authorize_admin_project!
-
- layout "project_settings"
-
- def edit
- end
-
- def update
- if ci_project.update_attributes(project_params)
- Ci::EventService.new.change_project_settings(current_user, ci_project)
-
- redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.'
- else
- render action: "edit"
- end
- end
-
- def destroy
- ci_project.destroy
- Ci::EventService.new.remove_project(current_user, ci_project)
- project.gitlab_ci_service.update_attributes(active: false)
-
- redirect_to project_path(project), notice: "CI was disabled for this project"
- end
-
- protected
-
- def project_params
- params.require(:project).permit(:path, :timeout, :timeout_in_minutes, :default_ref, :always_build,
- :polling_interval, :public, :ssh_url_to_repo, :allow_git_fetch, :email_recipients,
- :email_add_pusher, :email_only_broken_builds, :coverage_regex, :shared_runners_enabled, :token,
- { variables_attributes: [:id, :key, :value, :_destroy] })
- end
-end
diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb
deleted file mode 100644
index a2d470d4a69..00000000000
--- a/app/controllers/projects/ci_web_hooks_controller.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-class Projects::CiWebHooksController < Projects::ApplicationController
- before_action :ci_project
- before_action :authorize_admin_project!
-
- layout "project_settings"
-
- def index
- @web_hooks = @ci_project.web_hooks
- @web_hook = Ci::WebHook.new
- end
-
- def create
- @web_hook = @ci_project.web_hooks.new(web_hook_params)
- @web_hook.save
-
- if @web_hook.valid?
- redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
- else
- @web_hooks = @ci_project.web_hooks.select(&:persisted?)
- render :index
- end
- end
-
- def test
- Ci::TestHookService.new.execute(hook, current_user)
-
- redirect_back_or_default(default: { action: 'index' })
- end
-
- def destroy
- hook.destroy
-
- redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
- end
-
- private
-
- def hook
- @web_hook ||= @ci_project.web_hooks.find(params[:id])
- end
-
- def web_hook_params
- params.require(:web_hook).permit(:url)
- end
-end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 7886f3c6deb..0aaba3792bf 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -4,16 +4,17 @@
class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_download_code!, except: [:cancel_builds]
+ before_action :authorize_manage_builds!, only: [:cancel_builds]
before_action :commit
+ before_action :authorize_manage_builds!, only: [:cancel_builds, :retry_builds]
+ before_action :define_show_vars, only: [:show, :builds]
def show
return git_not_found! unless @commit
@line_notes = commit.notes.inline
- @diffs = @commit.diffs
@note = @project.build_commit_note(commit)
- @notes_count = commit.notes.count
@notes = commit.notes.not_inline.fresh
@noteable = @commit
@comments_allowed = @reply_allowed = true
@@ -22,8 +23,6 @@ class Projects::CommitController < Projects::ApplicationController
commit_id: @commit.id
}
- @ci_commit = project.ci_commit(commit.sha)
-
respond_to do |format|
format.html
format.diff { render text: @commit.to_diff }
@@ -31,20 +30,24 @@ class Projects::CommitController < Projects::ApplicationController
end
end
- def ci
- @ci_commit = @project.ci_commit(@commit.sha)
- @builds = @ci_commit.builds if @ci_commit
- @notes_count = @commit.notes.count
- @ci_project = @project.gitlab_ci_project
+ def builds
end
def cancel_builds
- @ci_commit = @project.ci_commit(@commit.sha)
- @ci_commit.builds.running_or_pending.each(&:cancel)
+ ci_commit.builds.running_or_pending.each(&:cancel)
- redirect_to ci_namespace_project_commit_path(project.namespace, project, commit.sha)
+ redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
end
+ def retry_builds
+ ci_commit.builds.latest.failed.each do |build|
+ if build.retryable?
+ Ci::Build.retry(build)
+ end
+ end
+
+ redirect_back_or_default default: builds_namespace_project_commit_path(project.namespace, project, commit.sha)
+ end
def branches
@branches = @project.repository.branch_names_contains(commit.id)
@@ -52,7 +55,31 @@ class Projects::CommitController < Projects::ApplicationController
render layout: false
end
+ private
+
def commit
@commit ||= @project.commit(params[:id])
end
+
+ def ci_commit
+ @ci_commit ||= project.ci_commit(commit.sha)
+ end
+
+ def define_show_vars
+ if params[:w].to_i == 1
+ @diffs = commit.diffs({ ignore_whitespace_change: true })
+ else
+ @diffs = commit.diffs
+ end
+
+ @notes_count = commit.notes.count
+
+ @statuses = ci_commit.statuses if ci_commit
+ end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, project)
+ return page_404
+ end
+ end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index d1c15174aea..04a88990bf4 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -9,10 +9,10 @@ class Projects::CommitsController < Projects::ApplicationController
def show
@repo = @project.repository
- @limit, @offset = (params[:limit] || 40), (params[:offset] || 0)
+ @limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
@commits = @repo.commits(@ref, @path, @limit, @offset)
- @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ @note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
respond_to do |format|
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 71aaad1fad6..5200d609cc9 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -12,15 +12,16 @@ class Projects::CompareController < Projects::ApplicationController
def show
base_ref = Addressable::URI.unescape(params[:from])
@ref = head_ref = Addressable::URI.unescape(params[:to])
+ diff_options = { ignore_whitespace_change: true } if params[:w] == '1'
compare_result = CompareService.new.
- execute(@project, head_ref, @project, base_ref)
+ execute(@project, head_ref, @project, base_ref, diff_options)
if compare_result
@commits = Commit.decorate(compare_result.commits, @project)
@diffs = compare_result.diffs
- @commit = @commits.last
- @first_commit = @commits.first
+ @commit = @project.commit(head_ref)
+ @first_commit = @project.commit(base_ref)
@line_notes = []
end
end
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 8a785076bb7..750181f0c19 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -10,19 +10,35 @@ class Projects::ForksController < Projects::ApplicationController
def create
namespace = Namespace.find(params[:namespace_key])
- @forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
+
+ @forked_project = namespace.projects.find_by(path: project.path)
+ @forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
+
+ @forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
if @forked_project.saved? && @forked_project.forked?
if @forked_project.import_in_progress?
- redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project)
+ redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project, continue: continue_params)
else
- redirect_to(
- namespace_project_path(@forked_project.namespace, @forked_project),
- notice: 'Project was successfully forked.'
- )
+ if continue_params
+ redirect_to continue_params[:to], notice: continue_params[:notice]
+ else
+ redirect_to namespace_project_path(@forked_project.namespace, @forked_project), notice: "The project was successfully forked."
+ end
end
else
render :error
end
end
+
+ private
+
+ def continue_params
+ continue_params = params[:continue]
+ if continue_params
+ continue_params.permit(:to, :notice, :notice_now)
+ else
+ nil
+ end
+ end
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 418b92040bc..d13ea9f34b6 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -5,7 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
- before_action :ci_enabled, only: :ci
+ before_action :builds_enabled, only: :ci
def show
respond_to do |format|
@@ -25,13 +25,31 @@ class Projects::GraphsController < Projects::ApplicationController
end
def ci
- ci_project = @project.gitlab_ci_project
-
@charts = {}
- @charts[:week] = Ci::Charts::WeekChart.new(ci_project)
- @charts[:month] = Ci::Charts::MonthChart.new(ci_project)
- @charts[:year] = Ci::Charts::YearChart.new(ci_project)
- @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project)
+ @charts[:week] = Ci::Charts::WeekChart.new(project)
+ @charts[:month] = Ci::Charts::MonthChart.new(project)
+ @charts[:year] = Ci::Charts::YearChart.new(project)
+ @charts[:build_times] = Ci::Charts::BuildTime.new(project)
+ end
+
+ def languages
+ @languages = Linguist::Repository.new(@repository.rugged, @repository.rugged.head.target_id).languages
+ total = @languages.map(&:last).sum
+
+ @languages = @languages.map do |language|
+ name, share = language
+ color = Digest::SHA256.hexdigest(name)[0...6]
+ {
+ value: (share.to_f * 100 / total).round(2),
+ label: name,
+ color: "##{color}",
+ highlight: "##{color}"
+ }
+ end
+
+ @languages.sort! do |x, y|
+ y[:value] <=> x[:value]
+ end
end
private
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index c7569541899..5fd4f855dec 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -25,13 +25,12 @@ class Projects::HooksController < Projects::ApplicationController
def test
if !@project.empty_repo?
- status = TestHookService.new.execute(hook, current_user)
+ status, message = TestHookService.new.execute(hook, current_user)
if status
flash[:notice] = 'Hook successfully executed.'
else
- flash[:alert] = 'Hook execution failed. '\
- 'Ensure hook URL is correct and service is up.'
+ flash[:alert] = "Hook execution failed: #{message}"
end
else
flash[:alert] = 'Hook execution failed. Ensure the project has commits.'
@@ -54,6 +53,7 @@ class Projects::HooksController < Projects::ApplicationController
def hook_params
params.require(:hook).permit(:url, :push_events, :issues_events,
- :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification)
+ :merge_requests_events, :tag_push_events, :note_events,
+ :build_events, :enable_ssl_verification)
end
end
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 066b66014f8..8d8035ef5ff 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,7 +1,7 @@
class Projects::ImportsController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
- before_action :require_no_repo
+ before_action :require_no_repo, except: :show
before_action :redirect_if_progress, except: :show
def new
@@ -24,21 +24,36 @@ class Projects::ImportsController < Projects::ApplicationController
end
def show
- unless @project.import_in_progress?
- if @project.import_finished?
- redirect_to(project_path(@project)) and return
+ if @project.repository_exists? || @project.import_finished?
+ if continue_params
+ redirect_to continue_params[:to], notice: continue_params[:notice]
else
- redirect_to new_namespace_project_import_path(@project.namespace,
- @project) && return
+ redirect_to project_path(@project), notice: "The project was successfully forked."
end
+ elsif @project.import_failed?
+ redirect_to new_namespace_project_import_path(@project.namespace, @project)
+ else
+ if continue_params && continue_params[:notice_now]
+ flash.now[:notice] = continue_params[:notice_now]
+ end
+ # Render
end
end
private
+ def continue_params
+ continue_params = params[:continue]
+ if continue_params
+ continue_params.permit(:to, :notice, :notice_now)
+ else
+ nil
+ end
+ end
+
def require_no_repo
if @project.repository_exists? && !@project.import_in_progress?
- redirect_to(namespace_project_path(@project.namespace, @project)) and return
+ redirect_to(namespace_project_path(@project.namespace, @project))
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e767efbdc0c..b59b52291fb 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -58,10 +58,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- @participants = @issue.participants(current_user)
@note = @project.notes.new(noteable: @issue)
- @notes = @issue.notes.with_associations.fresh
+ @notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
+ @merge_requests = @issue.referenced_merge_requests
respond_with(@issue)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 16c42386623..ab5c953189c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,13 +1,14 @@
class Projects::MergeRequestsController < Projects::ApplicationController
before_action :module_enabled
before_action :merge_request, only: [
- :edit, :update, :show, :diffs, :commits, :merge, :merge_check,
- :ci_status, :toggle_subscription
+ :edit, :update, :show, :diffs, :commits, :builds, :merge, :merge_check,
+ :ci_status, :toggle_subscription, :cancel_merge_when_build_succeeds
]
- before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits]
- before_action :validates_merge_request, only: [:show, :diffs, :commits]
- before_action :define_show_vars, only: [:show, :diffs, :commits]
- before_action :ensure_ref_fetched, only: [:show, :commits, :diffs]
+ before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits, :builds]
+ before_action :validates_merge_request, only: [:show, :diffs, :commits, :builds]
+ before_action :define_show_vars, only: [:show, :diffs, :commits, :builds]
+ before_action :define_widget_vars, only: [:merge, :cancel_merge_when_build_succeeds, :merge_check]
+ before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -31,6 +32,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
+ @merge_requests = @merge_requests.preload(:target_project)
respond_to do |format|
format.html
@@ -78,6 +80,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def builds
+ respond_to do |format|
+ format.html { render 'show' }
+ format.json { render json: { html: view_to_html_string('projects/merge_requests/show/_builds') } }
+ end
+ end
+
def new
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
@@ -90,20 +99,18 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@target_project = merge_request.target_project
@source_project = merge_request.source_project
- @commits = @merge_request.compare_commits
+ @commits = @merge_request.compare_commits.reverse
@commit = @merge_request.last_commit
@first_commit = @merge_request.first_commit
@diffs = @merge_request.compare_diffs
+
+ @ci_commit = @merge_request.ci_commit
+ @statuses = @ci_commit.statuses if @ci_commit
+
@note_counts = Note.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
end
- def edit
- @source_project = @merge_request.source_project
- @target_project = @merge_request.target_project
- @target_branches = @merge_request.target_project.repository.branch_names
- end
-
def create
@target_branches ||= []
@merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
@@ -117,6 +124,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
end
+ def edit
+ @source_project = @merge_request.source_project
+ @target_project = @merge_request.target_project
+ @target_branches = @merge_request.target_project.repository.branch_names
+ end
+
def update
@merge_request = MergeRequests::UpdateService.new(project, current_user, merge_request_params).execute(@merge_request)
@@ -140,24 +153,34 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_check
- if @merge_request.unchecked?
- @merge_request.check_if_can_be_merged
- end
-
- closes_issues
+ @merge_request.check_if_can_be_merged if @merge_request.unchecked?
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end
+ def cancel_merge_when_build_succeeds
+ return access_denied! unless @merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+
+ MergeRequests::MergeWhenBuildSucceedsService.new(@project, current_user).cancel(@merge_request)
+ end
+
def merge
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
- if @merge_request.mergeable?
- @merge_request.update(merge_error: nil)
- MergeWorker.perform_async(@merge_request.id, current_user.id, params)
- @status = true
+ unless @merge_request.mergeable?
+ @status = :failed
+ return
+ end
+
+ @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
else
- @status = false
+ MergeWorker.perform_async(@merge_request.id, current_user.id, params)
+ @status = :success
end
end
@@ -249,11 +272,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def define_show_vars
- @participants = @merge_request.participants(current_user)
-
# Build a note object for comment form
@note = @project.notes.new(noteable: @merge_request)
- @notes = @merge_request.mr_and_commit_notes.inc_author.fresh
+ @notes = @merge_request.mr_and_commit_notes.nonawards.inc_author.fresh
@discussions = Note.discussions_from_notes(@notes)
@noteable = @merge_request
@@ -263,12 +284,20 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request.merge_request_diff
+ @ci_commit = @merge_request.ci_commit
+ @statuses = @ci_commit.statuses if @ci_commit
+
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
end
end
+ def define_widget_vars
+ @ci_commit = @merge_request.ci_commit
+ closes_issues
+ end
+
def invalid_mr
# Render special view for MR with removed source or target branch
render 'invalid'
@@ -282,6 +311,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
+ def merge_params
+ params.permit(:should_remove_source_branch, :commit_message)
+ end
+
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 41cd08c93c6..6f1e186d408 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -3,7 +3,7 @@ class Projects::NotesController < Projects::ApplicationController
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]
+ before_action :find_current_user_notes, except: [:destroy, :delete_attachment, :award_toggle]
def index
current_fetched_at = Time.now.to_i
@@ -13,7 +13,8 @@ class Projects::NotesController < Projects::ApplicationController
@notes.each do |note|
notes_json[:notes] << {
id: note.id,
- html: note_to_html(note)
+ html: note_to_html(note),
+ valid: note.valid?
}
end
@@ -58,6 +59,30 @@ class Projects::NotesController < Projects::ApplicationController
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
+ end
+
+ render json: { ok: true }
+ end
+
private
def note
@@ -107,13 +132,24 @@ class Projects::NotesController < Projects::ApplicationController
end
def render_note_json(note)
- render json: {
- id: note.id,
- discussion_id: note.discussion_id,
- html: note_to_html(note),
- discussion_html: note_to_discussion_html(note),
- discussion_with_diff_html: note_to_discussion_with_diff_html(note)
- }
+ if note.valid?
+ render json: {
+ valid: true,
+ id: note.id,
+ discussion_id: note.discussion_id,
+ html: note_to_html(note),
+ award: note.is_award,
+ note: note.note,
+ discussion_html: note_to_discussion_html(note),
+ discussion_with_diff_html: note_to_discussion_with_diff_html(note)
+ }
+ else
+ render json: {
+ valid: false,
+ award: note.is_award,
+ errors: note.errors
+ }
+ end
end
def authorize_admin_note!
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 9de5269cd25..8364fc293b7 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,6 +1,6 @@
class Projects::ProjectMembersController < Projects::ApplicationController
# Authorize
- before_action :authorize_admin_project!, except: :leave
+ before_action :authorize_admin_project_member!, except: :leave
def index
@project_members = @project.project_members
@@ -23,16 +23,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.where(user_id: users)
end
- @group_members = @group_members.order('access_level DESC').limit(20)
+ @group_members = @group_members.order('access_level DESC')
end
@project_member = @project.project_members.new
end
- def new
- @project_member = @project.project_members.new
- end
-
def create
@project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
@@ -41,11 +37,17 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def update
@project_member = @project.project_members.find(params[:id])
+
+ return render_403 unless can?(current_user, :update_project_member, @project_member)
+
@project_member.update_attributes(member_params)
end
def destroy
@project_member = @project.project_members.find(params[:id])
+
+ return render_403 unless can?(current_user, :destroy_project_member, @project_member)
+
@project_member.destroy
respond_to do |format|
@@ -71,16 +73,22 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def leave
- if @project.namespace == current_user.namespace
- message = 'You can not leave your own project. Transfer or delete the project.'
- return redirect_back_or_default(default: { action: 'index' }, options: { alert: message })
- end
+ @project_member = @project.project_members.find_by(user_id: current_user)
- @project.project_members.find_by(user_id: current_user).destroy
+ if can?(current_user, :destroy_project_member, @project_member)
+ @project_member.destroy
- respond_to do |format|
- format.html { redirect_to dashboard_projects_path }
- format.js { render nothing: true }
+ 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
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 6b52eccebf7..e49259c34b6 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -21,7 +21,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
if protected_branch &&
protected_branch.update_attributes(
- developers_can_push: params[:developers_can_push]
+ developers_can_push: params[:developers_can_push]
)
respond_to do |format|
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index d5ee6ac8663..be7d5c187fe 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -10,15 +10,13 @@ class Projects::RawController < Projects::ApplicationController
@blob = @repository.blob_at(@commit.id, @path)
if @blob
- type = get_blob_type
-
headers['X-Content-Type-Options'] = 'nosniff'
- send_data(
- @blob.data,
- type: type,
- disposition: 'inline'
- )
+ if @blob.lfs_pointer?
+ send_lfs_object
+ else
+ stream_data
+ end
else
render_404
end
@@ -35,4 +33,33 @@ class Projects::RawController < Projects::ApplicationController
'application/octet-stream'
end
end
+
+ def stream_data
+ type = get_blob_type
+
+ send_data(
+ @blob.data,
+ type: type,
+ disposition: 'inline'
+ )
+ end
+
+ def send_lfs_object
+ lfs_object = find_lfs_object
+
+ if lfs_object && lfs_object.project_allowed_access?(@project)
+ send_file lfs_object.file.path, filename: @blob.name, disposition: 'attachment'
+ else
+ render_404
+ end
+ end
+
+ def find_lfs_object
+ lfs_object = LfsObject.find_by_oid(@blob.lfs_oid)
+ if lfs_object && lfs_object.file.exists?
+ lfs_object
+ else
+ nil
+ end
+ end
end
diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb
new file mode 100644
index 00000000000..0825a4311cb
--- /dev/null
+++ b/app/controllers/projects/releases_controller.rb
@@ -0,0 +1,31 @@
+class Projects::ReleasesController < Projects::ApplicationController
+ # Authorize
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
+ before_action :authorize_push_code!
+ before_action :tag
+ before_action :release
+
+ def edit
+ end
+
+ def update
+ release.update_attributes(release_params)
+
+ redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
+ end
+
+ private
+
+ def tag
+ @tag ||= @repository.find_tag(params[:tag_id])
+ end
+
+ def release
+ @release ||= @project.releases.find_or_initialize_by(tag: @tag.name)
+ end
+
+ def release_params
+ params.require(:release).permit(:description)
+ end
+end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
new file mode 100644
index 00000000000..e2785caa2fb
--- /dev/null
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -0,0 +1,26 @@
+class Projects::RunnerProjectsController < Projects::ApplicationController
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def create
+ @runner = Ci::Runner.find(params[:runner_project][:runner_id])
+
+ return head(403) unless current_user.ci_authorized_runners.include?(@runner)
+
+ path = runners_path(project)
+
+ if @runner.assign_to(project, current_user)
+ redirect_to path
+ else
+ redirect_to path, alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ runner_project = project.runner_projects.find(params[:id])
+ runner_project.destroy
+
+ redirect_to runners_path(project)
+ end
+end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index deb07a21416..4993b2648a5 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -1,16 +1,14 @@
class Projects::RunnersController < Projects::ApplicationController
- before_action :ci_project
before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
before_action :authorize_admin_project!
layout 'project_settings'
def index
- @runners = @ci_project.runners.order('id DESC')
- @specific_runners =
- Ci::Runner.specific.includes(:runner_projects).
- where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ).
- where.not(id: @runners).order("#{Ci::Runner.table_name}.id DESC").page(params[:page]).per(20)
+ @runners = project.runners.ordered
+ @specific_runners = current_user.ci_authorized_runners.
+ where.not(id: project.runners).
+ ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
@@ -27,7 +25,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def destroy
- if @runner.only_for?(@ci_project)
+ if @runner.only_for?(project)
@runner.destroy
end
@@ -53,10 +51,16 @@ class Projects::RunnersController < Projects::ApplicationController
def show
end
+ def toggle_shared_runners
+ project.toggle!(:shared_runners_enabled)
+
+ redirect_to namespace_project_runners_path(project.namespace, project)
+ end
+
protected
def set_runner
- @runner ||= @ci_project.runners.find(params[:id])
+ @runner ||= project.runners.find(params[:id])
end
def runner_params
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 42dbb497e01..8b2577aebe1 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -1,14 +1,17 @@
class Projects::ServicesController < Projects::ApplicationController
- ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain,
+ ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain,
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
:build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
- :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url,
+ :note_events, :build_events,
+ :notify_only_broken_builds, :add_pusher,
+ :send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
- :server_host, :server_port, :default_irc_uri, :enable_ssl_verification]
+ :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
+ :jira_issue_transition_id]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index b07a2a8db2f..2104c7a7a71 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -21,6 +21,7 @@ class Projects::SnippetsController < Projects::ApplicationController
filter: :by_project,
project: @project
})
+ @snippets = @snippets.page(params[:page]).per(PER_PAGE)
end
def new
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index f565fbbbbc3..280fe12cc7c 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -2,21 +2,29 @@ class Projects::TagsController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:create]
+ before_action :authorize_push_code!, only: [:new, :create]
before_action :authorize_admin_project!, only: [:destroy]
def index
sorted = VersionSorter.rsort(@repository.tag_names)
@tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE)
+ @releases = project.releases.where(tag: @tags)
+ end
+
+ def show
+ @tag = @repository.find_tag(params[:id])
+ @release = @project.releases.find_or_initialize_by(tag: @tag.name)
+ @commit = @repository.commit(@tag.target)
end
def create
result = CreateTagService.new(@project, current_user).
- execute(params[:tag_name], params[:ref], params[:message])
+ execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
@tag = result[:tag]
- redirect_to namespace_project_tags_path(@project.namespace, @project)
+
+ redirect_to namespace_project_tag_path(@project.namespace, @project, @tag.name)
else
@error = result[:message]
render action: 'new'
@@ -26,12 +34,6 @@ class Projects::TagsController < Projects::ApplicationController
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
- respond_to do |format|
- format.html do
- redirect_to namespace_project_tags_path(@project.namespace,
- @project)
- end
- format.js
- end
+ redirect_to namespace_project_tags_path(@project.namespace, @project)
end
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index bdcb1a3e297..cb3ed0f6f9c 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -1,13 +1,14 @@
# Controller for viewing a repository's file structure
class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
+ include CreatesCommit
include ActionView::Helpers::SanitizeHelper
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
before_action :assign_dir_vars, only: [:create_dir]
before_action :authorize_download_code!
- before_action :authorize_push_code!, only: [:create_dir]
+ before_action :authorize_edit_tree!, only: [:create_dir]
def show
return render_404 unless @repository.commit(@ref)
@@ -33,33 +34,19 @@ class Projects::TreeController < Projects::ApplicationController
def create_dir
return render_404 unless @commit_params.values.all?
- begin
- result = Files::CreateDirService.new(@project, current_user, @commit_params).execute
- message = result[:message]
- rescue => e
- message = e.to_s
- end
-
- if result && result[:status] == :success
- flash[:notice] = "The directory has been successfully created"
- respond_to do |format|
- format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@new_branch, @dir_name)) }
- end
- else
- flash[:alert] = message
- respond_to do |format|
- format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, @new_branch) }
- end
- end
+ create_commit(Files::CreateDirService, success_notice: "The directory has been successfully created.",
+ success_path: namespace_project_tree_path(@project.namespace, @project, File.join(@target_branch, @dir_name)),
+ failure_path: namespace_project_tree_path(@project.namespace, @project, @ref))
end
+ private
+
def assign_dir_vars
- @new_branch = params[:new_branch].present? ? sanitize(strip_tags(params[:new_branch])) : @ref
+ @target_branch = params[:target_branch]
+
@dir_name = File.join(@path, params[:dir_name])
@commit_params = {
file_path: @dir_name,
- current_branch: @ref,
- target_branch: @new_branch,
commit_message: params[:commit_message],
}
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
index 782ebd01b05..30adfad1daa 100644
--- a/app/controllers/projects/triggers_controller.rb
+++ b/app/controllers/projects/triggers_controller.rb
@@ -1,22 +1,21 @@
class Projects::TriggersController < Projects::ApplicationController
- before_action :ci_project
before_action :authorize_admin_project!
layout 'project_settings'
def index
- @triggers = @ci_project.triggers
+ @triggers = project.triggers
@trigger = Ci::Trigger.new
end
def create
- @trigger = @ci_project.triggers.new
+ @trigger = project.triggers.new
@trigger.save
if @trigger.valid?
redirect_to namespace_project_triggers_path(@project.namespace, @project)
else
- @triggers = @ci_project.triggers.select(&:persisted?)
+ @triggers = project.triggers.select(&:persisted?)
render :index
end
end
@@ -30,6 +29,6 @@ class Projects::TriggersController < Projects::ApplicationController
private
def trigger
- @trigger ||= @ci_project.triggers.find(params[:id])
+ @trigger ||= project.triggers.find(params[:id])
end
end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index d6561a45a70..10efafea9db 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -1,5 +1,4 @@
class Projects::VariablesController < Projects::ApplicationController
- before_action :ci_project
before_action :authorize_admin_project!
layout 'project_settings'
@@ -8,9 +7,7 @@ class Projects::VariablesController < Projects::ApplicationController
end
def update
- if ci_project.update_attributes(project_params)
- Ci::EventService.new.change_project_settings(current_user, ci_project)
-
+ if project.update_attributes(project_params)
redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
else
render action: 'show'
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index ecaf4476246..935f7d75c6a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,7 +1,7 @@
class ProjectsController < ApplicationController
include ExtractsPath
- prepend_before_filter :render_go_import, only: [:show]
+ prepend_before_action :render_go_import, only: [:show]
skip_before_action :authenticate_user!, only: [:show, :activity]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
@@ -72,8 +72,7 @@ class ProjectsController < ApplicationController
def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project)
- if @project.forked?
- @project.forked_project_link.destroy
+ if @project.unlink_fork
flash[:notice] = 'The fork relationship has been removed.'
end
end
@@ -124,11 +123,7 @@ class ProjectsController < ApplicationController
::Projects::DestroyService.new(@project, current_user, {}).execute
flash[:alert] = "Project '#{@project.name}' was deleted."
- if request.referer.include?('/admin')
- redirect_to admin_namespaces_projects_path
- else
- redirect_to dashboard_projects_path
- end
+ redirect_to dashboard_projects_path
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), alert: ex.message
end
@@ -185,14 +180,14 @@ class ProjectsController < ApplicationController
@project.reload
render json: {
- html: view_to_html_string("projects/buttons/_star")
+ star_count: @project.star_count
}
end
def markdown_preview
text = params[:text]
- ext = Gitlab::ReferenceExtractor.new(@project, current_user)
+ ext = Gitlab::ReferenceExtractor.new(@project, current_user, current_user)
ext.analyze(text)
render json: {
@@ -224,9 +219,10 @@ class ProjectsController < ApplicationController
def project_params
params.require(:project).permit(
- :name, :path, :description, :issues_tracker, :tag_list,
+ :name, :path, :description, :issues_tracker, :tag_list, :runners_token,
:issues_enabled, :merge_requests_enabled, :snippets_enabled, :issues_tracker_id, :default_branch,
- :wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar
+ :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,
)
end
@@ -255,7 +251,7 @@ class ProjectsController < ApplicationController
project.repository_exists? && !project.empty_repo?
end
- # Override get_id from ExtractsPath, which returns the branch and file path
+ # Override get_id 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.
def get_id
project.repository.root_ref
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 3b3dc86cb68..c48175a4c5a 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,10 +1,21 @@
class RegistrationsController < Devise::RegistrationsController
before_action :signup_enabled?
+ include Recaptcha::Verify
def new
redirect_to(new_user_session_path)
end
+ def create
+ if !Gitlab::Recaptcha.load_configurations! || verify_recaptcha
+ super
+ else
+ flash[:alert] = "There was an error with the reCAPTCHA code below. Please re-enter the code."
+ flash.delete :recaptcha_error
+ render action: 'new'
+ end
+ end
+
def destroy
DeleteUserService.new(current_user).execute(current_user)
@@ -38,4 +49,16 @@ class RegistrationsController < Devise::RegistrationsController
def sign_up_params
params.require(:user).permit(:username, :email, :name, :password, :password_confirmation)
end
+
+ def resource_name
+ :user
+ end
+
+ def resource
+ @resource ||= User.new(sign_up_params)
+ end
+
+ def devise_mapping
+ @devise_mapping ||= Devise.mappings[:user]
+ end
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index eb0408a95e5..9bb42ec86b3 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -23,8 +23,8 @@ class SearchController < ApplicationController
@search_results =
if @project
- unless %w(blobs notes issues merge_requests milestones wiki_blobs).
- include?(@scope)
+ unless %w(blobs notes issues merge_requests milestones wiki_blobs
+ commits).include?(@scope)
@scope = 'blobs'
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 1b60d3e27d0..825f85199be 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -1,9 +1,11 @@
class SessionsController < Devise::SessionsController
include AuthenticatesWithTwoFactor
+ include Recaptcha::ClientHelper
prepend_before_action :authenticate_with_two_factor, only: [:create]
prepend_before_action :store_redirect_path, only: [:new]
before_action :auto_sign_in_with_provider, only: [:new]
+ before_action :load_recaptcha
def new
if Gitlab.config.ldap.enabled
@@ -40,7 +42,7 @@ class SessionsController < Devise::SessionsController
User.find(session[:otp_user_id])
end
end
-
+
def store_redirect_path
redirect_path =
if request.referer.present? && (params['redirect_to_referer'] == 'yes')
@@ -87,14 +89,14 @@ class SessionsController < Devise::SessionsController
provider = Gitlab.config.omniauth.auto_sign_in_with_provider
return unless provider.present?
- # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
- # registered or no alert at all. In case of another alert (such as a blocked user), it is safer
+ # Auto sign in with an Omniauth provider only if the standard "you need to sign-in" alert is
+ # registered or no alert at all. In case of another alert (such as a blocked user), it is safer
# to do nothing to prevent redirection loops with certain Omniauth providers.
return unless flash[:alert].blank? || flash[:alert] == I18n.t('devise.failure.unauthenticated')
-
+
# Prevent alert from popping up on the first page shown after authentication.
- flash[:alert] = nil
-
+ flash[:alert] = nil
+
redirect_to user_omniauth_authorize_path(provider.to_sym)
end
@@ -107,4 +109,8 @@ class SessionsController < Devise::SessionsController
AuditEventService.new(user, user, options).
for_authentication.security_event
end
+
+ def load_recaptcha
+ Gitlab::Recaptcha.load_configurations!
+ end
end
diff --git a/app/controllers/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb
new file mode 100644
index 00000000000..682ca5e3821
--- /dev/null
+++ b/app/controllers/sherlock/application_controller.rb
@@ -0,0 +1,12 @@
+module Sherlock
+ class ApplicationController < ::ApplicationController
+ before_action :find_transaction
+
+ def find_transaction
+ if params[:transaction_id]
+ @transaction = Gitlab::Sherlock.collection.
+ find_transaction(params[:transaction_id])
+ end
+ end
+ end
+end
diff --git a/app/controllers/sherlock/file_samples_controller.rb b/app/controllers/sherlock/file_samples_controller.rb
new file mode 100644
index 00000000000..0c3bc100106
--- /dev/null
+++ b/app/controllers/sherlock/file_samples_controller.rb
@@ -0,0 +1,7 @@
+module Sherlock
+ class FileSamplesController < Sherlock::ApplicationController
+ def show
+ @file_sample = @transaction.find_file_sample(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/sherlock/queries_controller.rb b/app/controllers/sherlock/queries_controller.rb
new file mode 100644
index 00000000000..63b26aab1a4
--- /dev/null
+++ b/app/controllers/sherlock/queries_controller.rb
@@ -0,0 +1,7 @@
+module Sherlock
+ class QueriesController < Sherlock::ApplicationController
+ def show
+ @query = @transaction.find_query(params[:id])
+ end
+ end
+end
diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb
new file mode 100644
index 00000000000..ccc739da879
--- /dev/null
+++ b/app/controllers/sherlock/transactions_controller.rb
@@ -0,0 +1,19 @@
+module Sherlock
+ class TransactionsController < Sherlock::ApplicationController
+ def index
+ @transactions = Gitlab::Sherlock.collection.newest_first
+ end
+
+ def show
+ @transaction = Gitlab::Sherlock.collection.find_transaction(params[:id])
+
+ render_404 unless @transaction
+ end
+
+ def destroy_all
+ Gitlab::Sherlock.collection.clear
+
+ redirect_to(:back)
+ end
+ end
+end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 9f9f9a92f11..c72df73af46 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,6 +1,9 @@
class SnippetsController < ApplicationController
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ # Allow read snippet
+ before_action :authorize_read_snippet!, only: [:show, :raw]
+
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
@@ -79,10 +82,14 @@ class SnippetsController < ApplicationController
[Snippet::PUBLIC, Snippet::INTERNAL]).
find(params[:id])
else
- PersonalSnippet.are_public.find(params[:id])
+ PersonalSnippet.find(params[:id])
end
end
+ def authorize_read_snippet!
+ authenticate_user! unless can?(current_user, :read_personal_snippet, @snippet)
+ end
+
def authorize_update_snippet!
return render_404 unless can?(current_user, :update_personal_snippet, @snippet)
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 1484356a7f4..280228dbcc0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -3,14 +3,11 @@ class UsersController < ApplicationController
before_action :set_user
def show
- @contributed_projects = contributed_projects.joined(@user).
- reject(&:forked?)
+ @contributed_projects = contributed_projects.joined(@user).reject(&:forked?)
- @projects = @user.personal_projects.
- where(id: authorized_projects_ids).includes(:namespace)
+ @projects = PersonalProjectsFinder.new(@user).execute(current_user)
- # Collect only groups common for both users
- @groups = @user.groups & GroupsFinder.new.execute(current_user)
+ @groups = @user.groups.order_id_desc
respond_to do |format|
format.html
@@ -53,16 +50,8 @@ class UsersController < ApplicationController
@user = User.find_by_username!(params[:username])
end
- def authorized_projects_ids
- # Projects user can view
- @authorized_projects_ids ||=
- ProjectsFinder.new.execute(current_user).pluck(:id)
- end
-
def contributed_projects
- @contributed_projects = Project.
- where(id: authorized_projects_ids & @user.contributed_projects_ids).
- includes(:namespace)
+ ContributedProjectsFinder.new(@user).execute(current_user)
end
def contributions_calendar
@@ -73,9 +62,13 @@ class UsersController < ApplicationController
def load_events
# Get user activity feed for projects common for both users
@events = @user.recent_events.
- where(project_id: authorized_projects_ids).
- with_associations
+ merge(projects_for_current_user).
+ references(:project).
+ with_associations.
+ limit_recent(20, params[:offset])
+ end
- @events = @events.limit(20).offset(params[:offset] || 0)
+ def projects_for_current_user
+ ProjectsFinder.new.execute(current_user)
end
end
diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb
new file mode 100644
index 00000000000..0209649b017
--- /dev/null
+++ b/app/finders/contributed_projects_finder.rb
@@ -0,0 +1,37 @@
+class ContributedProjectsFinder
+ def initialize(user)
+ @user = user
+ end
+
+ # Finds the projects "@user" contributed to, limited to either public projects
+ # or projects visible to the given user.
+ #
+ # current_user - When given the list of the projects is limited to those only
+ # visible by this user.
+ #
+ # Returns an ActiveRecord::Relation.
+ def execute(current_user = nil)
+ if current_user
+ relation = projects_visible_to_user(current_user)
+ else
+ relation = public_projects
+ end
+
+ relation.includes(:namespace).order_id_desc
+ end
+
+ private
+
+ def projects_visible_to_user(current_user)
+ authorized = @user.contributed_projects.visible_to_user(current_user)
+
+ union = Gitlab::SQL::Union.
+ new([authorized.select(:id), public_projects.select(:id)])
+
+ Project.where("projects.id IN (#{union.to_sql})")
+ end
+
+ def public_projects
+ @user.contributed_projects.public_only
+ end
+end
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
deleted file mode 100644
index d3597ef0901..00000000000
--- a/app/finders/groups_finder.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-class GroupsFinder
- def execute(current_user, options = {})
- all_groups(current_user)
- end
-
- private
-
- def all_groups(current_user)
- if current_user
- if current_user.authorized_groups.any?
- # User has access to groups
- #
- # Return only:
- # groups with public projects
- # groups with internal projects
- # groups with joined projects
- #
- group_ids = Project.public_and_internal_only.pluck(:namespace_id) +
- current_user.authorized_groups.pluck(:id)
- Group.where(id: group_ids)
- else
- # User has no group membership
- #
- # Return only:
- # groups with public projects
- # groups with internal projects
- #
- Group.where(id: Project.public_and_internal_only.pluck(:namespace_id))
- end
- else
- # Not authenticated
- #
- # Return only:
- # groups with public projects
- Group.where(id: Project.public_only.pluck(:namespace_id))
- end
- end
-end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index c407dfc163a..3d5e8b6fbe7 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -62,10 +62,10 @@ class IssuableFinder
if project?
@project = Project.find(params[:project_id])
-
+
unless Ability.abilities.allowed?(current_user, :read_project, @project)
@project = nil
- end
+ end
else
@project = nil
end
@@ -77,11 +77,11 @@ class IssuableFinder
return @projects if defined?(@projects)
if project?
- project
+ @projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
- current_user.authorized_projects
+ @projects = current_user.authorized_projects
else
- ProjectsFinder.new.execute(current_user)
+ @projects = ProjectsFinder.new.execute(current_user)
end
end
@@ -190,8 +190,10 @@ class IssuableFinder
def by_project(items)
items =
- if projects
- items.of_projects(projects).references(:project)
+ if project?
+ items.of_projects(projects).references_project
+ elsif projects
+ items.merge(projects.reorder(nil)).join_project
else
items.none
end
@@ -206,7 +208,9 @@ class IssuableFinder
end
def sort(items)
- items.sort(params[:sort])
+ # 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)
end
def by_assignee(items)
diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb
new file mode 100644
index 00000000000..630c73c2a94
--- /dev/null
+++ b/app/finders/milestones_finder.rb
@@ -0,0 +1,12 @@
+class MilestonesFinder
+ def execute(projects, params)
+ milestones = Milestone.of_projects(projects)
+ milestones = milestones.reorder("due_date ASC")
+
+ case params[:state]
+ when 'closed' then milestones.closed
+ when 'all' then milestones
+ else milestones.active
+ end
+ end
+end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index ab252821b52..fa4c635f55c 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -12,9 +12,9 @@ class NotesFinder
when "commit"
project.notes.for_commit_id(target_id).not_inline
when "issue"
- project.issues.find(target_id).notes.inc_author
+ project.issues.find(target_id).notes.nonawards.inc_author
when "merge_request"
- project.merge_requests.find(target_id).mr_and_commit_notes.inc_author
+ project.merge_requests.find(target_id).mr_and_commit_notes.nonawards.inc_author
when "snippet", "project_snippet"
project.snippets.find(target_id).notes
else
diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb
new file mode 100644
index 00000000000..a61ffa22990
--- /dev/null
+++ b/app/finders/personal_projects_finder.rb
@@ -0,0 +1,41 @@
+class PersonalProjectsFinder
+ def initialize(user)
+ @user = user
+ end
+
+ # Finds the projects belonging to the user in "@user", limited to either
+ # public projects or projects visible to the given user.
+ #
+ # current_user - When given the list of projects is limited to those only
+ # visible by this user.
+ #
+ # Returns an ActiveRecord::Relation.
+ def execute(current_user = nil)
+ if current_user
+ relation = projects_visible_to_user(current_user)
+ else
+ relation = public_projects
+ end
+
+ relation.includes(:namespace).order_id_desc
+ end
+
+ private
+
+ def projects_visible_to_user(current_user)
+ authorized = @user.personal_projects.visible_to_user(current_user)
+
+ union = Gitlab::SQL::Union.
+ new([authorized.select(:id), public_and_internal_projects.select(:id)])
+
+ Project.where("projects.id IN (#{union.to_sql})")
+ end
+
+ def public_projects
+ @user.personal_projects.public_only
+ end
+
+ def public_and_internal_projects
+ @user.personal_projects.public_and_internal_only
+ end
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index c81bb51583a..3b4e0362e04 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -1,11 +1,39 @@
class ProjectsFinder
- def execute(current_user, options = {})
+ # Returns all projects, optionally including group projects a user has access
+ # to.
+ #
+ # ## Examples
+ #
+ # Retrieving all public projects:
+ #
+ # ProjectsFinder.new.execute
+ #
+ # Retrieving all public/internal projects and those the given user has access
+ # to:
+ #
+ # ProjectsFinder.new.execute(some_user)
+ #
+ # Retrieving all public/internal projects as well as the group's projects the
+ # user has access to:
+ #
+ # ProjectsFinder.new.execute(some_user, group: some_group)
+ #
+ # Returns an ActiveRecord::Relation.
+ def execute(current_user = nil, options = {})
group = options[:group]
if group
- group_projects(current_user, group)
+ segments = group_projects(current_user, group)
else
- all_projects(current_user)
+ segments = all_projects(current_user)
+ end
+
+ if segments.length > 1
+ union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) })
+
+ Project.where("projects.id IN (#{union.to_sql})")
+ else
+ segments.first
end
end
@@ -13,77 +41,36 @@ class ProjectsFinder
def group_projects(current_user, group)
if current_user
- if group.users.include?(current_user)
- # User is group member
- #
- # Return ALL group projects
- group.projects
- else
- projects_members = ProjectMember.in_projects(group.projects).
- with_user(current_user)
-
- if projects_members.any?
- # User is a project member
- #
- # Return only:
- # public projects
- # internal projects
- # joined projects
- #
- group.projects.where(
- "projects.id IN (?) OR projects.visibility_level IN (?)",
- projects_members.pluck(:source_id),
- Project.public_and_internal_levels
- )
- else
- # User has no access to group or group projects
- #
- # Return only:
- # public projects
- # internal projects
- #
- group.projects.public_and_internal_only
- end
- end
+ [
+ group_projects_for_user(current_user, group),
+ group.projects.public_and_internal_only
+ ]
else
- # Not authenticated
- #
- # Return only:
- # public projects
- group.projects.public_only
+ [group.projects.public_only]
end
end
def all_projects(current_user)
if current_user
- if current_user.authorized_projects.any?
- # User has access to private projects
- #
- # Return only:
- # public projects
- # internal projects
- # joined projects
- #
- Project.where(
- "projects.id IN (?) OR projects.visibility_level IN (?)",
- current_user.authorized_projects.pluck(:id),
- Project.public_and_internal_levels
- )
- else
- # User has no access to private projects
- #
- # Return only:
- # public projects
- # internal projects
- #
- Project.public_and_internal_only
- end
+ [current_user.authorized_projects, public_and_internal_projects]
else
- # Not authenticated
- #
- # Return only:
- # public projects
- Project.public_only
+ [Project.public_only]
end
end
+
+ def group_projects_for_user(current_user, group)
+ if group.users.include?(current_user)
+ group.projects
+ else
+ group.projects.visible_to_user(current_user)
+ end
+ end
+
+ def public_projects
+ Project.unscoped.public_only
+ end
+
+ def public_and_internal_projects
+ Project.unscoped.public_and_internal_only
+ end
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 8ecdeaf8e76..f7f7a1a02d3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -61,29 +61,29 @@ module ApplicationHelper
options[:class] ||= ''
options[:class] << ' identicon'
bg_key = project.id % 7
- style = "background-color: ##{ allowed_colors.values[bg_key] }; color: #555"
+ style = "background-color: ##{allowed_colors.values[bg_key]}; color: #555"
content_tag(:div, class: options[:class], style: style) do
project.name[0, 1].upcase
end
end
- def avatar_icon(user_or_email = nil, size = nil)
+ def avatar_icon(user_or_email = nil, size = nil, scale = 2)
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email)
+ user = User.find_by(email: user_or_email.downcase)
end
if user
user.avatar_url(size) || default_avatar
else
- gravatar_icon(user_or_email, size)
+ gravatar_icon(user_or_email, size, scale)
end
end
- def gravatar_icon(user_email = '', size = nil)
- GravatarService.new.execute(user_email, size) ||
+ def gravatar_icon(user_email = '', size = nil, scale = 2)
+ GravatarService.new.execute(user_email, size, scale) ||
default_avatar
end
@@ -204,12 +204,16 @@ module ApplicationHelper
# Returns an HTML-safe String
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
- class: "#{html_class} js-timeago",
+ class: "#{html_class} js-timeago js-timeago-pending",
datetime: time.getutc.iso8601,
title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
- element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
+ unless skip_js
+ element << javascript_tag(
+ "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
+ )
+ end
element
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index cd99a232403..0cfc0565e84 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,5 +1,5 @@
module AuthHelper
- PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
@@ -50,5 +50,17 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s)
end
+ def two_factor_skippable?
+ current_application_settings.require_two_factor_authentication &&
+ !current_user.two_factor_enabled &&
+ current_application_settings.two_factor_grace_period &&
+ !two_factor_grace_period_expired?
+ end
+
+ def two_factor_grace_period_expired?
+ current_user.otp_grace_period_started_at &&
+ (current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
+ end
+
extend self
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 77d99140c43..d31d4cde08f 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -22,36 +22,92 @@ module BlobHelper
%w(credits changelog news copying copyright license authors)
end
- def edit_blob_link(project, ref, path, options = {})
- blob =
- begin
- project.repository.blob_at(ref, path)
- rescue
- nil
- end
-
- if blob && blob.text?
- text = 'Edit'
- after = options[:after] || ''
- from_mr = options[:from_merge_request_id]
- link_opts = {}
- link_opts[:from_merge_request_id] = from_mr if from_mr
- cls = 'btn btn-small'
- if allowed_tree_edit?(project, ref)
- link_to(text,
- namespace_project_edit_blob_path(project.namespace, project,
- tree_join(ref, path),
- link_opts),
- class: cls
- )
- else
- content_tag :span, text, class: cls + ' disabled'
- end + after.html_safe
- else
- ''
+ def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
+ return unless current_user
+
+ blob = project.repository.blob_at(ref, path) rescue nil
+
+ return unless blob && blob_text_viewable?(blob)
+
+ from_mr = options[:from_merge_request_id]
+ link_opts = {}
+ link_opts[:from_merge_request_id] = from_mr if from_mr
+
+ edit_path = namespace_project_edit_blob_path(project.namespace, project,
+ tree_join(ref, path),
+ link_opts)
+
+ if !on_top_of_branch?
+ button_tag "Edit", class: "btn btn-default disabled has_tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
+ elsif can_edit_blob?(blob)
+ link_to "Edit", edit_path, class: 'btn btn-small'
+ elsif can?(current_user, :fork_project, project)
+ continue_params = {
+ to: edit_path,
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to "Edit", fork_path, class: 'btn btn-small', method: :post
+ end
+ end
+
+ def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
+ return unless current_user
+
+ blob = project.repository.blob_at(ref, path) rescue nil
+
+ return unless blob
+
+ if !on_top_of_branch?
+ button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "You can only #{action} files when you are on a branch", data: { container: 'body' }
+ elsif blob.lfs_pointer?
+ button_tag label, class: "btn btn-#{btn_class} disabled has_tooltip", title: "It is not possible to #{action} files that are stored in LFS using the web interface", data: { container: 'body' }
+ elsif can_edit_blob?(blob)
+ button_tag label, class: "btn btn-#{btn_class}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
+ elsif can?(current_user, :fork_project, project)
+ continue_params = {
+ to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to #{action} this file again.",
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_fork_path(project.namespace, project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to label, fork_path, class: "btn btn-#{btn_class}", method: :post
end
end
+ def replace_blob_link(project = @project, ref = @ref, path = @path)
+ modify_file_link(
+ project,
+ ref,
+ path,
+ label: "Replace",
+ action: "replace",
+ btn_class: "default",
+ modal_type: "upload"
+ )
+ end
+
+ def delete_blob_link(project = @project, ref = @ref, path = @path)
+ modify_file_link(
+ project,
+ ref,
+ path,
+ label: "Delete",
+ action: "delete",
+ btn_class: "remove",
+ modal_type: "remove"
+ )
+ end
+
+ def can_edit_blob?(blob, project = @project, ref = @ref)
+ !blob.lfs_pointer? && can_edit_tree?(project, ref)
+ end
+
def leave_edit_message
"Leave edit mode?\nAll unsaved changes will be lost."
end
@@ -60,7 +116,7 @@ module BlobHelper
if Gitlab::MarkupHelper.previewable?(filename)
'Preview'
else
- 'Preview changes'
+ 'Preview Changes'
end
end
@@ -71,4 +127,16 @@ module BlobHelper
def blob_icon(mode, name)
icon("#{file_type_icon_class('file', mode, name)} fw")
end
+
+ def blob_text_viewable?(blob)
+ blob && blob.text? && !blob.lfs_pointer?
+ end
+
+ def blob_size(blob)
+ if blob.lfs_pointer?
+ blob.lfs_size
+ else
+ blob.size
+ end
+ end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index d6eaa7d57bc..e39548e17e1 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -11,7 +11,7 @@ module BranchesHelper
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name)
-
+
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
deleted file mode 100644
index 1b5a2c31d74..00000000000
--- a/app/helpers/builds_helper.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module BuildsHelper
- def build_ref_link build
- gitlab_ref_link build.project, build.ref
- end
-
- def build_commit_link build
- gitlab_commit_link build.project, build.short_sha
- end
-
- def build_url(build)
- namespace_project_build_path(build.gl_project, build.project, build)
- end
-end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
new file mode 100644
index 00000000000..ec0e3f409c1
--- /dev/null
+++ b/app/helpers/button_helper.rb
@@ -0,0 +1,58 @@
+module ButtonHelper
+ # Output a "Copy to Clipboard" button
+ #
+ # data - Data attributes passed to `content_tag`
+ #
+ # Examples:
+ #
+ # # Define the clipboard's text
+ # clipboard_button(clipboard_text: "Foo")
+ # # => "<button class='...' data-clipboard-text='Foo'>...</button>"
+ #
+ # # Define the target element
+ # clipboard_button(clipboard_target: "div#foo")
+ # # => "<button class='...' data-clipboard-target='div#foo'>...</button>"
+ #
+ # See http://clipboardjs.com/#usage
+ def clipboard_button(data = {})
+ content_tag :button,
+ icon('clipboard'),
+ class: 'btn btn-xs btn-clipboard',
+ data: data,
+ type: :button
+ end
+
+ def http_clone_button(project)
+ klass = 'btn js-protocol-switch'
+ klass << ' active' if default_clone_protocol == 'http'
+ klass << ' has_tooltip' if current_user.try(:require_password?)
+
+ protocol = gitlab_config.protocol.upcase
+
+ content_tag :button, protocol,
+ class: klass,
+ data: {
+ clone: project.http_url_to_repo,
+ container: 'body',
+ html: 'true',
+ title: "Set a password on your account<br>to pull or push via #{protocol}"
+ },
+ type: :button
+ end
+
+ def ssh_clone_button(project)
+ klass = 'btn js-protocol-switch'
+ klass << ' active' if default_clone_protocol == 'ssh'
+ klass << ' has_tooltip' if current_user.try(:require_ssh_key?)
+
+ content_tag :button, 'SSH',
+ class: klass,
+ data: {
+ clone: project.ssh_url_to_repo,
+ container: 'body',
+ html: 'true',
+ title: 'Add an SSH key to your profile<br>to pull or push via SSH.'
+ },
+ type: :button
+ end
+end
diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb
deleted file mode 100644
index baddbc806f2..00000000000
--- a/app/helpers/ci/gitlab_helper.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- module GitlabHelper
- def no_turbolink
- { :"data-no-turbolink" => "data-no-turbolink" }
- end
-
- def gitlab_ref_link project, ref
- gitlab_url = project.gitlab_url.dup
- gitlab_url << "/commits/#{ref}"
- link_to ref, gitlab_url, no_turbolink
- end
-
- def gitlab_compare_link project, before, after
- gitlab_url = project.gitlab_url.dup
- gitlab_url << "/compare/#{before}...#{after}"
-
- link_to "#{before}...#{after}", gitlab_url, no_turbolink
- end
-
- def gitlab_commit_link project, sha
- gitlab_url = project.gitlab_url.dup
- gitlab_url << "/commit/#{sha}"
- link_to Ci::Commit.truncate_sha(sha), gitlab_url, no_turbolink
- end
-
- def yaml_web_editor_link(project)
- commits = project.commits
-
- if commits.any? && commits.last.ci_yaml_file
- "#{project.gitlab_url}/edit/master/.gitlab-ci.yml"
- else
- "#{project.gitlab_url}/new/master"
- end
- end
- end
-end
diff --git a/app/helpers/ci/projects_helper.rb b/app/helpers/ci/projects_helper.rb
deleted file mode 100644
index fd991a4165a..00000000000
--- a/app/helpers/ci/projects_helper.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module Ci
- module ProjectsHelper
- def ref_tab_class ref = nil
- 'active' if ref == @ref
- end
-
- def success_ratio(success_builds, failed_builds)
- failed_builds = failed_builds.count(:all)
- success_builds = success_builds.count(:all)
-
- return 100 if failed_builds.zero?
-
- ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
- ratio.to_i
- end
-
- def markdown_badge_code(project, ref)
- url = status_ci_project_url(project, ref: ref, format: 'png')
- "[![build status](#{url})](#{ci_project_url(project, ref: ref)})"
- end
-
- def html_badge_code(project, ref)
- url = status_ci_project_url(project, ref: ref, format: 'png')
- "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>"
- end
-
- def project_uses_specific_runner?(project)
- project.runners.any?
- end
-
- def no_runners_for_project?(project)
- project.runners.blank? &&
- Ci::Runner.shared.blank?
- end
- end
-end
diff --git a/app/helpers/ci_badge_helper.rb b/app/helpers/ci_badge_helper.rb
new file mode 100644
index 00000000000..27386133e36
--- /dev/null
+++ b/app/helpers/ci_badge_helper.rb
@@ -0,0 +1,13 @@
+module CiBadgeHelper
+ def markdown_badge_code(project, ref)
+ url = status_ci_project_url(project, ref: ref, format: 'png')
+ link = namespace_project_commits_path(project.namespace, project, ref)
+ "[![build status](#{url})](#{link})"
+ end
+
+ def html_badge_code(project, ref)
+ url = status_ci_project_url(project, ref: ref, format: 'png')
+ link = namespace_project_commits_path(project.namespace, project, ref)
+ "<a href='#{link}'><img src='#{url}' /></a>"
+ end
+end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index dbd1e26fa79..d8bee21c82e 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -1,29 +1,28 @@
module CiStatusHelper
def ci_status_path(ci_commit)
- project = ci_commit.gl_project
- ci_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
+ project = ci_commit.project
+ builds_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
end
def ci_status_icon(ci_commit)
ci_icon_for_status(ci_commit.status)
end
- def ci_status_color(ci_commit)
- case ci_commit.status
- when 'success'
- 'green'
- when 'failed'
- 'red'
- when 'running', 'pending'
- 'yellow'
- else
- 'gray'
- end
+ def ci_status_label(ci_commit)
+ ci_label_for_status(ci_commit.status)
end
def ci_status_with_icon(status)
content_tag :span, class: "ci-status ci-#{status}" do
- ci_icon_for_status(status) + '&nbsp;'.html_safe + status
+ ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
+ end
+ end
+
+ def ci_label_for_status(status)
+ if status == 'success'
+ 'passed'
+ else
+ status
end
end
@@ -40,6 +39,19 @@ module CiStatusHelper
'circle'
end
- icon(icon_name)
+ icon(icon_name + ' fw')
+ end
+
+ def render_ci_status(ci_commit)
+ link_to ci_status_icon(ci_commit),
+ ci_status_path(ci_commit),
+ class: "ci-status-link ci-status-icon-#{ci_commit.status.dasherize}",
+ title: "Build #{ci_status_label(ci_commit)}",
+ data: { toggle: 'tooltip', placement: 'left' }
+ end
+
+ def no_runners_for_project?(project)
+ project.runners.blank? &&
+ Ci::Runner.shared.blank?
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 9df20c9fce5..590d20ac7b3 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -109,7 +109,7 @@ module CommitsHelper
)
elsif @path.present?
return link_to(
- "Browse Dir »",
+ "Browse Directory »",
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "pull-right"
@@ -117,7 +117,7 @@ module CommitsHelper
end
end
link_to(
- "Browse Code »",
+ "Browse Files »",
namespace_project_tree_path(project.namespace, project, commit),
class: "pull-right"
)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index e65e37211c4..24134310fc5 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -1,4 +1,8 @@
module DiffHelper
+ def diff_view
+ params[:view] == 'parallel' ? 'parallel' : 'inline'
+ end
+
def allowed_diff_size
if diff_hard_limit_enabled?
Commit::DIFF_HARD_LIMIT_FILES
@@ -132,33 +136,19 @@ module DiffHelper
end
def inline_diff_btn
- params_copy = params.dup
- params_copy[:view] = 'inline'
- # Always use HTML to handle case where JSON diff rendered this button
- params_copy.delete(:format)
-
- link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
- 'Inline'
- end
+ diff_btn('Inline', 'inline', diff_view == 'inline')
end
def parallel_diff_btn
- params_copy = params.dup
- params_copy[:view] = 'parallel'
- # Always use HTML to handle case where JSON diff rendered this button
- params_copy.delete(:format)
-
- link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
- 'Side-by-side'
- end
+ diff_btn('Side-by-side', 'parallel', diff_view == 'parallel')
end
def submodule_link(blob, ref, repository = @repository)
tree, commit = submodule_links(blob, ref, repository)
commit_id = if commit.nil?
- blob.id[0..10]
+ Commit.truncate_sha(blob.id)
else
- link_to "#{blob.id[0..10]}", commit
+ link_to Commit.truncate_sha(blob.id), commit
end
[
@@ -171,7 +161,7 @@ module DiffHelper
def commit_for_diff(diff)
if diff.deleted_file
first_commit = @first_commit || @commit
- first_commit.parent
+ first_commit.parent || @first_commit
else
@commit
end
@@ -187,4 +177,18 @@ module DiffHelper
def editable_diff?(diff)
!diff.deleted_file && @merge_request && @merge_request.source_project
end
+
+ private
+
+ def diff_btn(title, name, selected)
+ params_copy = params.dup
+ params_copy[:view] = name
+
+ # Always use HTML to handle case where JSON diff rendered this button
+ params_copy.delete(:format)
+
+ link_to url_for(params_copy), id: "#{name}-diff-btn", class: (selected ? 'btn active' : 'btn') do
+ title
+ end
+ end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 45788ba95ac..41b5bd7be90 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -28,6 +28,8 @@ module EmailsHelper
return "View #{action.humanize.singularize}"
end
end
+
+ nil
end
def color_email_diff(diffcontent)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 6f69c2a9f32..dde83ff36b5 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -108,19 +108,23 @@ module EventsHelper
end
end
elsif event.push?
- if event.push_with_commits? && event.md_ref?
- if event.commits_count > 1
- namespace_project_compare_url(event.project.namespace, event.project,
- from: event.commit_from, to:
- event.commit_to)
- else
- namespace_project_commit_url(event.project.namespace, event.project,
- id: event.commit_to)
- end
+ push_event_feed_url(event)
+ end
+ end
+
+ def push_event_feed_url(event)
+ if event.push_with_commits? && event.md_ref?
+ if event.commits_count > 1
+ namespace_project_compare_url(event.project.namespace, event.project,
+ from: event.commit_from, to:
+ event.commit_to)
else
- namespace_project_commits_url(event.project.namespace, event.project,
- event.ref_name)
+ namespace_project_commit_url(event.project.namespace, event.project,
+ id: event.commit_to)
end
+ else
+ namespace_project_commits_url(event.project.namespace, event.project,
+ event.ref_name)
end
end
@@ -198,7 +202,7 @@ module EventsHelper
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: avatar_icon(event.author_email)
+ 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
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
index 838b85afdfe..1f3401f2906 100644
--- a/app/helpers/external_wiki_helper.rb
+++ b/app/helpers/external_wiki_helper.rb
@@ -1,7 +1,7 @@
module ExternalWikiHelper
def get_project_wiki_path(project)
external_wiki_service = project.services.
- select { |service| service.to_param == 'external_wiki' }.first
+ find { |service| service.to_param == 'external_wiki' }
if external_wiki_service.present? && external_wiki_service.active?
external_wiki_service.properties['external_wiki_url']
else
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 65813482120..ca41657cec1 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -20,7 +20,7 @@ module GitlabMarkdownHelper
end
user = current_user if defined?(current_user)
- gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: user)
+ gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -48,47 +48,34 @@ module GitlabMarkdownHelper
def markdown(text, context = {})
return "" unless text.present?
- context.reverse_merge!(
- path: @path,
- pipeline: :default,
- project: @project,
- project_wiki: @project_wiki,
- ref: @ref
- )
+ context[:project] ||= @project
- user = current_user if defined?(current_user)
+ html = Banzai.render(text, context)
- html = Gitlab::Markdown.render(text, context)
- Gitlab::Markdown.post_process(html, pipeline: context[:pipeline], project: @project, user: user)
- end
+ context.merge!(
+ current_user: (current_user if defined?(current_user)),
- # TODO (rspeicher): Remove all usages of this helper and just call `markdown`
- # with a custom pipeline depending on the content being rendered
- def gfm(text, options = {})
- return "" unless text.present?
-
- options.reverse_merge!(
- path: @path,
- pipeline: :default,
- project: @project,
- project_wiki: @project_wiki,
- ref: @ref
+ # RelativeLinkFilter
+ requested_path: @path,
+ project_wiki: @project_wiki,
+ ref: @ref
)
- user = current_user if defined?(current_user)
-
- html = Gitlab::Markdown.gfm(text, options)
- Gitlab::Markdown.post_process(html, pipeline: options[:pipeline], project: @project, user: user)
+ Banzai.post_process(html, context)
end
def asciidoc(text)
- Gitlab::Asciidoc.render(text, {
- commit: @commit,
- project: @project,
- project_wiki: @project_wiki,
+ Gitlab::Asciidoc.render(
+ text,
+ project: @project,
+ current_user: (current_user if defined?(current_user)),
+
+ # RelativeLinkFilter
+ project_wiki: @project_wiki,
requested_path: @path,
- ref: @ref
- })
+ ref: @ref,
+ commit: @commit
+ )
end
# Return the first line of +text+, up to +max_chars+, after parsing the line
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index b0b536d4649..f3fddef01cb 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -6,7 +6,7 @@
#
# For example instead of this:
#
-# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.projects, merge_request)
+# namespace_project_merge_request_path(merge_request.project.namespace, merge_request.project, merge_request)
#
# We can simply use shortcut:
#
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index 1e372d5631d..c2ab80f2e0d 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -16,4 +16,14 @@ module GraphHelper
ids = parents.map { |p| p.id }
ids.zip(parent_spaces)
end
+
+ def success_ratio(success_builds, failed_builds)
+ failed_builds = failed_builds.count(:all)
+ success_builds = success_builds.count(:all)
+
+ return 100 if failed_builds.zero?
+
+ ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
+ ratio.to_i
+ end
end
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 1cf5b96481a..5724d3aabec 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -27,16 +27,20 @@ module IconsHelper
end
end
- def public_icon
- icon('globe fw')
- end
-
- def internal_icon
- icon('shield fw')
- end
+ def visibility_level_icon(level, fw: true)
+ name =
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ 'lock'
+ when Gitlab::VisibilityLevel::INTERNAL
+ 'shield'
+ else # Gitlab::VisibilityLevel::PUBLIC
+ 'globe'
+ end
+
+ name << " fw" if fw
- def private_icon
- icon('lock fw')
+ icon(name)
end
def file_type_icon_class(type, mode, name)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index fda18e7b316..c12456a187f 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -44,28 +44,35 @@ module IssuesHelper
end
def bulk_update_milestone_options
- options_for_select([['None (backlog)', -1]]) +
- options_from_collection_for_select(project_active_milestones, 'id',
- 'title', params[:milestone_id])
+ milestones = project_active_milestones.to_a
+ milestones.unshift(Milestone::None)
+
+ options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
end
def milestone_options(object)
- options_from_collection_for_select(object.project.milestones.active,
- 'id', 'title', object.milestone_id)
+ milestones = object.project.milestones.active.to_a
+ milestones.unshift(Milestone::None)
+
+ options_from_collection_for_select(milestones, 'id', 'title', object.milestone_id)
end
- def issue_box_class(item)
+ def status_box_class(item)
if item.respond_to?(:expired?) && item.expired?
- 'issue-box-expired'
+ 'status-box-expired'
elsif item.respond_to?(:merged?) && item.merged?
- 'issue-box-merged'
+ 'status-box-merged'
elsif item.closed?
- 'issue-box-closed'
+ 'status-box-closed'
else
- 'issue-box-open'
+ 'status-box-open'
end
end
+ def issue_button_visibility(issue, closed)
+ 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,
@@ -74,7 +81,7 @@ module IssuesHelper
issue.project, issue)
xml.title truncate(issue.title, length: 80)
xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(issue.author_email)
+ 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
@@ -84,9 +91,51 @@ module IssuesHelper
end
def merge_requests_sentence(merge_requests)
- merge_requests.map(&:to_reference).to_sentence(last_word_connector: ', or ')
+ # Sorting based on the `!123` or `group/project!123` reference will sort
+ # local merge requests first.
+ merge_requests.map do |merge_request|
+ merge_request.to_reference(@project)
+ end.sort.to_sentence(last_word_connector: ', or ')
+ end
+
+ def emoji_icon(name, unicode = nil, aliases = [])
+ unicode ||= Emoji.emoji_filename(name) rescue ""
+
+ content_tag :div, "",
+ class: "icon emoji-icon emoji-#{unicode}",
+ "data-emoji" => name,
+ "data-aliases" => aliases.join(" "),
+ "data-unicode-name" => unicode
+ end
+
+ def emoji_author_list(notes, current_user)
+ list = notes.map do |note|
+ note.author == current_user ? "me" : note.author.name
+ end
+
+ list.join(", ")
+ end
+
+ def note_active_class(notes, current_user)
+ if current_user && notes.pluck(:author_id).include?(current_user.id)
+ "active"
+ else
+ ""
+ end
+ end
+
+ def awards_sort(awards)
+ awards.sort_by do |award, notes|
+ if award == "thumbsup"
+ 0
+ elsif award == "thumbsdown"
+ 1
+ else
+ 2
+ end
+ end.to_h
end
- # Required for Gitlab::Markdown::IssueReferenceFilter
+ # Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index ee04ace35d0..a2c3d4d2f32 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -100,13 +100,13 @@ module LabelsHelper
Label.where(project_id: @projects)
end
- grouped_labels = Labels::GroupService.new(labels).execute
+ grouped_labels = GlobalLabel.build_collection(labels)
grouped_labels.unshift(Label::None)
grouped_labels.unshift(Label::Any)
options_from_collection_for_select(grouped_labels, 'name', 'title', params[:label_name])
end
- # Required for Gitlab::Markdown::LabelReferenceFilter
+ # Required for Banzai::Filter::LabelReferenceFilter
module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 728d877ace2..1dd07a2a220 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -8,14 +8,6 @@ module MergeRequestsHelper
)
end
- def new_mr_path_for_fork_from_push_event(event)
- new_namespace_project_merge_request_path(
- event.project.namespace,
- event.project,
- new_mr_from_push_event(event, event.project.forked_from_project)
- )
- end
-
def new_mr_from_push_event(event, target_project)
{
merge_request: {
@@ -35,7 +27,16 @@ module MergeRequestsHelper
end
def ci_build_details_path(merge_request)
- 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.last_commit.sha, merge_request.source_branch)
+ return nil unless build_url
+
+ parsed_url = URI.parse(build_url)
+
+ unless parsed_url.userinfo.blank?
+ parsed_url.userinfo = ''
+ end
+
+ parsed_url.to_s
end
def merge_path_description(merge_request, separator)
@@ -47,7 +48,11 @@ module MergeRequestsHelper
end
def issues_sentence(issues)
- issues.map(&:to_reference).to_sentence
+ # Sorting based on the `#123` or `group/project#123` reference will sort
+ # local issues first.
+ issues.map do |issue|
+ issue.to_reference(@project)
+ end.sort.to_sentence
end
def mr_change_branches_path(merge_request)
@@ -57,18 +62,21 @@ module MergeRequestsHelper
source_project_id: @merge_request.source_project_id,
target_project_id: @merge_request.target_project_id,
source_branch: @merge_request.source_branch,
- target_branch: nil
- }
+ target_branch: @merge_request.target_branch,
+ },
+ change_branches: true
)
end
def source_branch_with_namespace(merge_request)
+ branch = link_to(merge_request.source_branch, namespace_project_commits_path(merge_request.source_project.namespace, merge_request.source_project, merge_request.source_branch))
+
if merge_request.for_fork?
namespace = link_to(merge_request.source_project_namespace,
project_path(merge_request.source_project))
- namespace + ":#{merge_request.source_branch}"
+ namespace + ":" + branch
else
- merge_request.source_branch
+ branch
end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 37a5b58cce8..a42cbcff182 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -28,7 +28,9 @@ module MilestonesHelper
Milestone.where(project_id: @projects)
end.active
- grouped_milestones = Milestones::GroupService.new(milestones).execute
+ epoch = DateTime.parse('1970-01-01')
+ grouped_milestones = GlobalMilestone.build_collection(milestones)
+ grouped_milestones = grouped_milestones.sort_by { |x| x.due_date.nil? ? epoch : x.due_date }
grouped_milestones.unshift(Milestone::None)
grouped_milestones.unshift(Milestone::Any)
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index b3132a1f3ba..faba418c4db 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -1,10 +1,10 @@
module NamespacesHelper
- def namespaces_options(selected = :current_user, scope = :default)
+ def namespaces_options(selected = :current_user, display_path: false)
groups = current_user.owned_groups + current_user.masters_groups
users = [current_user.namespace]
- group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [g.human_name, g.id]} ]
- users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [u.human_name, u.id]} ]
+ group_opts = ["Groups", groups.sort_by(&:human_name).map {|g| [display_path ? g.path : g.human_name, g.id]} ]
+ users_opts = [ "Users", users.sort_by(&:human_name).map {|u| [display_path ? u.path : u.human_name, u.id]} ]
options = []
options << group_opts
@@ -17,15 +17,6 @@ module NamespacesHelper
grouped_options_for_select(options, selected)
end
- def namespace_select_tag(id, opts = {})
- css_class = "ajax-namespace-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << (opts[:class] || '')
- value = opts[:selected] || ''
-
- hidden_field_tag(id, value, class: css_class)
- end
-
def namespace_icon(namespace, size = 40)
if namespace.kind_of?(Group)
group_icon(namespace)
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 9b1dd8b8e54..e6fb8670e57 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -5,6 +5,14 @@ module NavHelper
def nav_sidebar_class
if nav_menu_collapsed?
+ "sidebar-collapsed"
+ else
+ "sidebar-expanded"
+ end
+ end
+
+ def page_sidebar_class
+ if nav_menu_collapsed?
"page-sidebar-collapsed"
else
"page-sidebar-expanded"
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index cf11f8e5320..499c655d2bf 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -16,40 +16,28 @@ module NotificationsHelper
def notification_list_item(notification_level, user_membership)
case notification_level
when Notification::N_DISABLED
- content_tag(:li, class: active_level_for(user_membership, Notification::N_DISABLED)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_DISABLED } do
- icon('microphone-slash fw', text: 'Disabled')
- end
- end
+ update_notification_link(Notification::N_DISABLED, user_membership, 'Disabled', 'microphone-slash')
when Notification::N_PARTICIPATING
- content_tag(:li, class: active_level_for(user_membership, Notification::N_PARTICIPATING)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_PARTICIPATING } do
- icon('volume-up fw', text: 'Participate')
- end
- end
+ update_notification_link(Notification::N_PARTICIPATING, user_membership, 'Participate', 'volume-up')
when Notification::N_WATCH
- content_tag(:li, class: active_level_for(user_membership, Notification::N_WATCH)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_WATCH } do
- icon('eye fw', text: 'Watch')
- end
- end
+ update_notification_link(Notification::N_WATCH, user_membership, 'Watch', 'eye')
when Notification::N_MENTION
- content_tag(:li, class: active_level_for(user_membership, Notification::N_MENTION)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_MENTION } do
- icon('at fw', text: 'On mention')
- end
- end
+ update_notification_link(Notification::N_MENTION, user_membership, 'On mention', 'at')
when Notification::N_GLOBAL
- content_tag(:li, class: active_level_for(user_membership, Notification::N_GLOBAL)) do
- link_to '#', class: 'update-notification', data: { notification_level: Notification::N_GLOBAL } do
- icon('globe fw', text: 'Global')
- end
- end
+ update_notification_link(Notification::N_GLOBAL, user_membership, 'Global', 'globe')
else
# do nothing
end
end
+ def update_notification_link(notification_level, user_membership, title, icon)
+ content_tag(:li, class: active_level_for(user_membership, notification_level)) do
+ link_to '#', class: 'update-notification', data: { notification_level: notification_level } do
+ icon("#{icon} fw", text: title)
+ end
+ end
+ end
+
def notification_label(user_membership)
Notification.new(user_membership).to_s
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 775cf5a3dd4..791cb9e50bd 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -4,7 +4,82 @@ module PageLayoutHelper
@page_title.push(*titles.compact) if titles.any?
- @page_title.join(" | ")
+ # Segments are seperated by middot
+ @page_title.join(" \u00b7 ")
+ end
+
+ # Define or get a description for the current page
+ #
+ # description - String (default: nil)
+ #
+ # If this helper is called multiple times with an argument, only the last
+ # description will be returned when called without an argument. Descriptions
+ # have newlines replaced with spaces and all HTML tags are sanitized.
+ #
+ # Examples:
+ #
+ # page_description # => "GitLab Community Edition"
+ # page_description("Foo")
+ # page_description # => "Foo"
+ #
+ # page_description("<b>Bar</b>\nBaz")
+ # page_description # => "Bar Baz"
+ #
+ # Returns an HTML-safe String.
+ def page_description(description = nil)
+ @page_description ||= page_description_default
+
+ if description.present?
+ @page_description = description.squish
+ else
+ sanitize(@page_description, tags: []).truncate_words(30)
+ end
+ end
+
+ # Default value for page_description when one hasn't been defined manually by
+ # a view
+ def page_description_default
+ if @project
+ @project.description || brand_title
+ else
+ brand_title
+ end
+ end
+
+ def page_image
+ default = image_url('gitlab_logo.png')
+
+ if @project
+ @project.avatar_url || default
+ elsif @user
+ avatar_icon(@user)
+ else
+ default
+ end
+ end
+
+ # Define or get attributes to be used as Twitter card metadata
+ #
+ # map - Hash of label => data pairs. Keys become labels, values become data
+ #
+ # Raises ArgumentError if given more than two attributes
+ def page_card_attributes(map = {})
+ 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
+ end
+
+ def page_card_meta_tags
+ tags = ''
+
+ page_card_attributes.each_with_index do |pair, i|
+ tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
+ tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
+ end
+
+ tags.html_safe
end
def header_title(title = nil, title_url = nil)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 5301c2ccf76..77ba612548a 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -21,7 +21,7 @@ module ProjectsHelper
end
def link_to_member(project, author, opts = {})
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author' }
+ default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
opts = default_opts.merge(opts)
return "(deleted)" unless author
@@ -39,7 +39,8 @@ module ProjectsHelper
if opts[:name]
link_to(author_html, user_path(author), class: "author_link").html_safe
else
- link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => sanitize(author.name) } ).html_safe
+ title = opts[:title].sub(":name", sanitize(author.name))
+ link_to(author_html, user_path(author), class: "author_link has_tooltip", data: { :'original-title' => title, container: 'body' } ).html_safe
end
end
@@ -104,6 +105,14 @@ 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
+
private
def get_project_nav_tabs(project, current_user)
@@ -117,7 +126,7 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if project.gitlab_ci? && can?(current_user, :read_build, project)
+ if project.builds_enabled? && can?(current_user, :read_build, project)
nav_tabs << :builds
end
@@ -173,13 +182,20 @@ module ProjectsHelper
'unknown'
end
- def default_url_to_repo(project = nil)
- project = project || @project
- current_user ? project.url_to_repo : project.http_url_to_repo
+ def default_url_to_repo(project = @project)
+ if default_clone_protocol == "ssh"
+ project.ssh_url_to_repo
+ else
+ project.http_url_to_repo
+ end
end
def default_clone_protocol
- current_user ? "ssh" : "http"
+ if !current_user || current_user.require_ssh_key?
+ "http"
+ else
+ "ssh"
+ end
end
def project_last_activity(project)
@@ -253,14 +269,6 @@ module ProjectsHelper
filename_path(project, :version)
end
- def hidden_pass_url(original_url)
- result = URI(original_url)
- result.password = '*****' unless result.password.nil?
- result
- rescue
- original_url
- end
-
def project_wiki_path_with_version(proj, page, version, is_newest)
url_params = is_newest ? {} : { version_id: version }
namespace_project_wiki_path(proj.namespace, proj, page, url_params)
@@ -277,14 +285,6 @@ module ProjectsHelper
end
end
- def user_max_access_in_project(user, project)
- level = project.team.max_member_access(user)
-
- if level
- Gitlab::Access.options_with_owner.key(level)
- end
- end
-
def leave_project_message(project)
"Are you sure you want to leave \"#{project.name}\" project?"
end
@@ -330,10 +330,9 @@ module ProjectsHelper
def filename_path(project, filename)
if project && blob = project.repository.send(filename)
namespace_project_blob_path(
- project.namespace,
- project,
- tree_join(project.default_branch,
- blob.name)
+ project.namespace,
+ project,
+ tree_join(project.default_branch, blob.name)
)
end
end
diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb
index 46eb82a354f..9fb42487a75 100644
--- a/app/helpers/runners_helper.rb
+++ b/app/helpers/runners_helper.rb
@@ -19,7 +19,7 @@ module RunnersHelper
id = "\##{runner.id}"
if current_user && current_user.admin
- link_to ci_admin_runner_path(runner) do
+ link_to admin_runner_path(runner) do
display_name + id
end
else
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index c31a556ff7b..d4f78258626 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -70,7 +70,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- current_user.authorized_groups.search(term).limit(limit).map do |group|
+ Group.search(term).limit(limit).map do |group|
{
label: "group: #{search_result_sanitize(group.name)}",
url: group_path(group)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 12fce8db701..05386d790ca 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -15,12 +15,14 @@ module SelectsHelper
html = {
class: css_class,
- 'data-placeholder' => placeholder,
- 'data-null-user' => null_user,
- 'data-any-user' => any_user,
- 'data-email-user' => email_user,
- 'data-first-user' => first_user,
- 'data-current-user' => current_user
+ data: {
+ placeholder: placeholder,
+ null_user: null_user,
+ any_user: any_user,
+ email_user: email_user,
+ first_user: first_user,
+ current_user: current_user
+ }
}
unless opts[:scope] == :all
@@ -35,8 +37,33 @@ module SelectsHelper
end
def groups_select_tag(id, opts = {})
- css_class = "ajax-groups-select "
- css_class << "multiselect " if opts[:multiple]
+ opts[:class] ||= ''
+ opts[:class] << ' ajax-groups-select'
+ select2_tag(id, opts)
+ end
+
+ def namespace_select_tag(id, opts = {})
+ opts[:class] ||= ''
+ opts[:class] << ' ajax-namespace-select'
+ select2_tag(id, opts)
+ end
+
+ def project_select_tag(id, opts = {})
+ opts[:class] ||= ''
+ opts[:class] << ' ajax-project-select'
+
+ unless opts.delete(:scope) == :all
+ if @group
+ opts['data-group-id'] = @group.id
+ end
+ end
+
+ hidden_field_tag(id, opts[:selected], opts)
+ end
+
+ def select2_tag(id, opts = {})
+ css_class = ''
+ css_class << 'multiselect ' if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 0e7d8065ac7..04e53fe7c61 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -110,22 +110,4 @@ module TabHelper
'active'
end
end
-
- # Use nav_tab for save controller/action but different params
- def nav_tab(key, value, &block)
- o = {}
- o[:class] = ""
-
- if value.nil?
- o[:class] << " active" if params[key].blank?
- else
- o[:class] << " active" if params[key] == value
- end
-
- if block_given?
- content_tag(:li, capture(&block), o)
- else
- content_tag(:li, nil, o)
- end
- end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 03a49e119b8..2ad7c80dae0 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -46,12 +46,51 @@ module TreeHelper
File.join(*args)
end
- def allowed_tree_edit?(project = nil, ref = nil)
+ def on_top_of_branch?(project = @project, ref = @ref)
+ project.repository.branch_names.include?(ref)
+ end
+
+ def can_edit_tree?(project = nil, ref = nil)
project ||= @project
ref ||= @ref
- return false unless project.repository.branch_names.include?(ref)
- ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
+ return false unless on_top_of_branch?(project, ref)
+
+ can?(current_user, :push_code, project) ||
+ (current_user && current_user.already_forked?(project))
+ end
+
+ def tree_edit_branch(project = @project, ref = @ref)
+ return unless can_edit_tree?(project, ref)
+
+ if can_push_branch?(project, ref)
+ ref
+ else
+ project = tree_edit_project(project)
+ project.repository.next_patch_branch
+ end
+ end
+
+ def tree_edit_project(project = @project)
+ if can?(current_user, :push_code, project)
+ project
+ elsif current_user && current_user.already_forked?(project)
+ current_user.fork_of(project)
+ end
+ end
+
+ def edit_in_new_fork_notice_now
+ "You're not allowed to make changes to this project directly." +
+ " A fork of this project is being created that you can make changes in, so you can submit a merge request."
+ end
+
+ def edit_in_new_fork_notice
+ "You're not allowed to make changes to this project directly." +
+ " A fork of this project has been created that you can make changes in, so you can submit a merge request."
+ end
+
+ def commit_in_fork_help
+ "A new branch will be created in your fork and a new merge request will be started."
end
def tree_breadcrumbs(tree, max_links = 2)
@@ -65,7 +104,7 @@ module TreeHelper
part_path = File.join(part_path, part) unless part_path.empty?
part_path = part if part_path.empty?
- next unless parts.last(2).include?(part) if parts.count > max_links
+ next if parts.count > max_links && !parts.last(2).include?(part)
yield(part, tree_join(@ref, part_path))
end
end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
index 2a3a7e80fca..8cad994d10f 100644
--- a/app/helpers/triggers_helper.rb
+++ b/app/helpers/triggers_helper.rb
@@ -1,5 +1,5 @@
module TriggersHelper
- def ci_build_trigger_url(project_id, ref_name)
- "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
+ def builds_trigger_url(project_id)
+ "#{Settings.gitlab.url}/api/v3/projects/#{project_id}/trigger/builds"
end
end
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index b52cd23aba2..71d33b445c2 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -12,61 +12,41 @@ module VisibilityLevelHelper
# Return the description for the +level+ argument.
#
- # +level+ One of the Gitlab::VisibilityLevel constants
- # +form_model+ Either a model object (Project, Snippet, etc.) or the name of
- # a Project or Snippet class.
+ # +level+ One of the Gitlab::VisibilityLevel constants
+ # +form_model+ Either a model object (Project, Snippet, etc.) or the name of
+ # a Project or Snippet class.
def visibility_level_description(level, form_model)
- case form_model.is_a?(String) ? form_model : form_model.class.name
- when 'PersonalSnippet', 'ProjectSnippet', 'Snippet'
- snippet_visibility_level_description(level)
- when 'Project'
+ case form_model
+ when Project
project_visibility_level_description(level)
+ when Snippet
+ snippet_visibility_level_description(level, form_model)
end
end
def project_visibility_level_description(level)
- capture_haml do
- haml_tag :span do
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "Project access must be granted explicitly for each user."
- when Gitlab::VisibilityLevel::INTERNAL
- haml_concat "The project can be cloned by"
- haml_concat "any logged in user."
- when Gitlab::VisibilityLevel::PUBLIC
- haml_concat "The project can be cloned"
- haml_concat "without any"
- haml_concat "authentication."
- end
- end
- end
- end
-
- def snippet_visibility_level_description(level)
- capture_haml do
- haml_tag :span do
- case level
- when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "The snippet is visible only for me."
- when Gitlab::VisibilityLevel::INTERNAL
- haml_concat "The snippet is visible for any logged in user."
- when Gitlab::VisibilityLevel::PUBLIC
- haml_concat "The snippet can be accessed"
- haml_concat "without any"
- haml_concat "authentication."
- end
- end
+ case level
+ when Gitlab::VisibilityLevel::PRIVATE
+ "Project access must be granted explicitly to each user."
+ when Gitlab::VisibilityLevel::INTERNAL
+ "The project can be cloned by any logged in user."
+ when Gitlab::VisibilityLevel::PUBLIC
+ "The project can be cloned without any authentication."
end
end
- def visibility_level_icon(level)
+ def snippet_visibility_level_description(level, snippet = nil)
case level
when Gitlab::VisibilityLevel::PRIVATE
- private_icon
+ if snippet.is_a? ProjectSnippet
+ "The snippet is visible only to project members."
+ else
+ "The snippet is visible only to me."
+ end
when Gitlab::VisibilityLevel::INTERNAL
- internal_icon
+ "The snippet is visible to any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
- public_icon
+ "The snippet can be accessed without any authentication."
end
end
@@ -89,7 +69,6 @@ module VisibilityLevelHelper
def skip_level?(form_model, level)
form_model.is_a?(Project) &&
- form_model.forked? &&
- !Gitlab::VisibilityLevel.allowed_fork_levels(form_model.forked_from_project.visibility_level).include?(level)
+ !form_model.visibility_level_allowed?(level)
end
end
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index f0c41f69a5c..d0ce827a595 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -2,11 +2,19 @@ class AbuseReportMailer < BaseMailer
include Gitlab::CurrentSettings
def notify(abuse_report_id)
+ return unless deliverable?
+
@abuse_report = AbuseReport.find(abuse_report_id)
mail(
- to: current_application_settings.admin_notification_email,
+ to: current_application_settings.admin_notification_email,
subject: "#{@abuse_report.user.name} (#{@abuse_report.user.username}) was reported for abuse"
)
end
+
+ private
+
+ def deliverable?
+ current_application_settings.admin_notification_email.present?
+ end
end
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index aedb0889185..8b83bbd93b7 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -8,10 +8,6 @@ class BaseMailer < ActionMailer::Base
default from: Proc.new { default_sender_address.format }
default reply_to: Proc.new { default_reply_to_address.format }
- def self.delay
- delay_for(2.seconds)
- end
-
def can?
Ability.abilities.allowed?(current_user, action, subject)
end
diff --git a/app/mailers/ci/emails/builds.rb b/app/mailers/ci/emails/builds.rb
deleted file mode 100644
index 6fb4fba85e5..00000000000
--- a/app/mailers/ci/emails/builds.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Ci
- module Emails
- module Builds
- def build_fail_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
- mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
- end
-
- def build_success_email(build_id, to)
- @build = Ci::Build.find(build_id)
- @project = @build.project
- mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
- end
- end
- end
-end
diff --git a/app/mailers/ci/notify.rb b/app/mailers/ci/notify.rb
deleted file mode 100644
index 404842cf213..00000000000
--- a/app/mailers/ci/notify.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-module Ci
- class Notify < ActionMailer::Base
- include Ci::Emails::Builds
-
- add_template_helper Ci::GitlabHelper
-
- default_url_options[:host] = Gitlab.config.gitlab.host
- default_url_options[:protocol] = Gitlab.config.gitlab.protocol
- default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
- default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
-
- default from: Gitlab.config.gitlab.email_from
-
- # Just send email with 3 seconds delay
- def self.delay
- delay_for(2.seconds)
- end
-
- private
-
- # Formats arguments into a String suitable for use as an email subject
- #
- # extra - Extra Strings to be inserted into the subject
- #
- # Examples
- #
- # >> subject('Lorem ipsum')
- # => "GitLab-CI | Lorem ipsum"
- #
- # # Automatically inserts Project name when @project is set
- # >> @project = Project.last
- # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
- # >> subject('Lorem ipsum')
- # => "GitLab-CI | Ruby on Rails | Lorem ipsum "
- #
- # # Accepts multiple arguments
- # >> subject('Lorem ipsum', 'Dolor sit amet')
- # => "GitLab-CI | Lorem ipsum | Dolor sit amet"
- def subject(*extra)
- subject = "GitLab-CI"
- subject << (@project ? " | #{@project.name}" : "")
- subject << " | " + extra.join(' | ') if extra.present?
- subject
- end
- end
-end
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
new file mode 100644
index 00000000000..d58609a2de5
--- /dev/null
+++ b/app/mailers/emails/builds.rb
@@ -0,0 +1,15 @@
+module Emails
+ module Builds
+ def build_fail_email(build_id, to)
+ @build = Ci::Build.find(build_id)
+ @project = @build.project
+ mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
+ end
+
+ def build_success_email(build_id, to)
+ @build = Ci::Build.find(build_id)
+ @project = @build.project
+ mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
+ end
+ end
+end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 2c035fbb70b..abdeefed5ef 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,53 +1,49 @@
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
- @issue = Issue.find(issue_id)
- @project = @issue.project
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_new_thread(@issue,
- from: sender(@issue.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ issue_mail_with_notification(issue_id, recipient_id) do
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
+ end
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
- @issue = Issue.find(issue_id)
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- @project = @issue.project
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_answer_thread(@issue,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ issue_mail_with_notification(issue_id, recipient_id) do
+ @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
- @issue = Issue.find issue_id
- @project = @issue.project
- @updated_by = User.find updated_by_user_id
- @target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_answer_thread(@issue,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ issue_mail_with_notification(issue_id, recipient_id) do
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- @issue = Issue.find issue_id
- @issue_status = status
+ issue_mail_with_notification(issue_id, recipient_id) do
+ @issue_status = status
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
+ end
+ end
+
+ private
+
+ def issue_thread_options(sender_id, recipient_id)
+ {
+ from: sender(sender_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@issue.title} (##{@issue.iid})")
+ }
+ end
+
+ def issue_mail_with_notification(issue_id, recipient_id)
+ @issue = Issue.find(issue_id)
@project = @issue.project
- @updated_by = User.find updated_by_user_id
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- mail_answer_thread(@issue,
- from: sender(updated_by_user_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
+
+ yield
SentNotification.record(@issue, recipient_id, reply_key)
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 87ba94a583d..e1382d2da12 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,47 +1,52 @@
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @commit = @note.noteable
- @project = @note.project
- @target_url = namespace_project_commit_url(@project.namespace, @project,
- @commit, anchor:
- "note_#{@note.id}")
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
+ note_mail_with_notification(note_id, recipient_id) do
+ @commit = @note.noteable
+ @target_url = namespace_project_commit_url(*note_target_url_options)
- SentNotification.record_note(@note, recipient_id, reply_key)
+ mail_answer_thread(@commit,
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@commit.title} (#{@commit.short_id})"))
+ end
end
def note_issue_email(recipient_id, note_id)
- @note = Note.find(note_id)
- @issue = @note.noteable
- @project = @note.project
- @target_url = namespace_project_issue_url(@project.namespace, @project,
- @issue, anchor:
- "note_#{@note.id}")
- mail_answer_thread(@issue,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@issue.title} (##{@issue.iid})"))
-
- SentNotification.record_note(@note, recipient_id, reply_key)
+ note_mail_with_notification(note_id, recipient_id) do
+ @issue = @note.noteable
+ @target_url = namespace_project_issue_url(*note_target_url_options)
+ mail_answer_thread(@issue, note_thread_options(recipient_id))
+ end
end
def note_merge_request_email(recipient_id, note_id)
+ note_mail_with_notification(note_id, recipient_id) do
+ @merge_request = @note.noteable
+ @target_url = namespace_project_merge_request_url(*note_target_url_options)
+ mail_answer_thread(@merge_request, note_thread_options(recipient_id))
+ end
+ end
+
+ private
+
+ def note_target_url_options
+ [@project.namespace, @project, @note.noteable, anchor: "note_#{@note.id}"]
+ end
+
+ def note_thread_options(recipient_id)
+ {
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})")
+ }
+ end
+
+ def note_mail_with_notification(note_id, recipient_id)
@note = Note.find(note_id)
- @merge_request = @note.noteable
@project = @note.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request, anchor:
- "note_#{@note.id}")
- mail_answer_thread(@merge_request,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+
+ yield
SentNotification.record_note(@note, recipient_id, reply_key)
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index caba63006da..b96418679bd 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -59,85 +59,17 @@ module Emails
subject: subject("Project was moved"))
end
- def repository_push_email(project_id, recipient, author_id: nil,
- ref: nil,
- action: nil,
- compare: nil,
- reverse_compare: false,
- send_from_committer_email: false,
- disable_diffs: false)
- unless author_id && ref && action
- raise ArgumentError, "missing keywords: author_id, ref, action"
- end
+ def repository_push_email(project_id, recipient, opts = {})
+ @message =
+ Gitlab::Email::Message::RepositoryPush.new(self, project_id, recipient, opts)
- @project = Project.find(project_id)
- @current_user = @author = User.find(author_id)
- @reverse_compare = reverse_compare
- @compare = compare
- @ref_name = Gitlab::Git.ref_name(ref)
- @ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch"
- @action = action
- @disable_diffs = disable_diffs
-
- if @compare
- @commits = Commit.decorate(compare.commits, @project)
- @diffs = compare.diffs
- end
-
- @action_name =
- case action
- when :create
- "pushed new"
- when :delete
- "deleted"
- else
- "pushed to"
- end
-
- @subject = "[Git]"
- @subject << "[#{@project.path_with_namespace}]"
- @subject << "[#{@ref_name}]" if action == :push
- @subject << " "
-
- if action == :push
- if @commits.length > 1
- @target_url = namespace_project_compare_url(@project.namespace,
- @project,
- from: Commit.new(@compare.base, @project),
- to: Commit.new(@compare.head, @project))
- @subject << "Deleted " if @reverse_compare
- @subject << "#{@commits.length} commits: #{@commits.first.title}"
- else
- @target_url = namespace_project_commit_url(@project.namespace,
- @project, @commits.first)
-
- @subject << "Deleted 1 commit: " if @reverse_compare
- @subject << @commits.first.title
- end
- else
- unless action == :delete
- @target_url = namespace_project_tree_url(@project.namespace,
- @project, @ref_name)
- end
-
- subject_action = @action_name.dup
- subject_action[0] = subject_action[0].capitalize
- @subject << "#{subject_action} #{@ref_type} #{@ref_name}"
- end
-
- @disable_footer = true
-
- reply_to =
- if send_from_committer_email && can_send_from_user_email?(@author)
- @author.email
- else
- Gitlab.config.gitlab.email_reply_to
- end
-
- mail(from: sender(author_id, send_from_committer_email),
- reply_to: reply_to,
- to: recipient,
- subject: @subject)
+ # used in notify layout
+ @target_url = @message.target_url
+
+ mail(from: sender(@message.author_id, @message.send_from_committer_email?),
+ reply_to: @message.reply_to,
+ to: @message.recipient,
+ subject: @message.subject)
end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 50a409c3754..3bbdd9cee76 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -7,6 +7,7 @@ class Notify < BaseMailer
include Emails::Projects
include Emails::Profile
include Emails::Groups
+ include Emails::Builds
add_template_helper MergeRequestsHelper
add_template_helper EmailsHelper
@@ -16,7 +17,7 @@ class Notify < BaseMailer
subject: subject,
body: body.html_safe,
content_type: 'text/html'
- )
+ )
end
# Splits "gitlab.corp.company.com" up into "gitlab.corp.company.com",
@@ -33,13 +34,13 @@ class Notify < BaseMailer
allowed_domains
end
- private
-
def can_send_from_user_email?(sender)
sender_domain = sender.email.split("@").last
self.class.allowed_email_domains.include?(sender_domain)
end
+ private
+
# Return an email address that displays the name of the sender.
# Only the displayed name changes; the actual email address is always the same.
def sender(sender_id, send_from_user_email = false)
diff --git a/app/models/ability.rb b/app/models/ability.rb
index b72178fa126..5a1a67db8e1 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -1,8 +1,8 @@
class Ability
class << self
def allowed(user, subject)
- return not_auth_abilities(user, subject) if user.nil?
- return [] unless user.kind_of?(User)
+ return anonymous_abilities(user, subject) if user.nil?
+ return [] unless user.is_a?(User)
return [] if user.blocked?
case subject.class.name
@@ -15,19 +15,30 @@ class Ability
when "Group" then group_abilities(user, subject)
when "Namespace" then namespace_abilities(user, subject)
when "GroupMember" then group_member_abilities(user, subject)
+ when "ProjectMember" then project_member_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
- # List of possible abilities
- # for non-authenticated user
- def not_auth_abilities(user, subject)
- project = if subject.kind_of?(Project)
+ # List of possible abilities for anonymous user
+ def anonymous_abilities(user, subject)
+ case true
+ when subject.is_a?(PersonalSnippet)
+ anonymous_personal_snippet_abilities(subject)
+ when subject.is_a?(Project) || subject.respond_to?(:project)
+ anonymous_project_abilities(subject)
+ when subject.is_a?(Group) || subject.respond_to?(:group)
+ anonymous_group_abilities(subject)
+ else
+ []
+ end
+ end
+
+ def anonymous_project_abilities(subject)
+ project = if subject.is_a?(Project)
subject
- elsif subject.respond_to?(:project)
- subject.project
else
- nil
+ subject.project
end
if project && project.public?
@@ -47,19 +58,29 @@ class Ability
rules - project_disabled_features_rules(project)
else
- group = if subject.kind_of?(Group)
- subject
- elsif subject.respond_to?(:group)
- subject.group
- else
- nil
- end
+ []
+ end
+ end
- if group && group.public_profile?
- [:read_group]
- else
- []
- end
+ def anonymous_group_abilities(subject)
+ group = if subject.is_a?(Group)
+ subject
+ else
+ subject.group
+ end
+
+ if group && group.projects.public_only.any?
+ [:read_group]
+ else
+ []
+ end
+ end
+
+ def anonymous_personal_snippet_abilities(snippet)
+ if snippet.public?
+ [:read_personal_snippet]
+ else
+ []
end
end
@@ -111,14 +132,14 @@ class Ability
end
def public_project_rules
- project_guest_rules + [
+ @public_project_rules ||= project_guest_rules + [
:download_code,
:fork_project
]
end
def project_guest_rules
- [
+ @project_guest_rules ||= [
:read_project,
:read_wiki,
:read_issue,
@@ -136,7 +157,7 @@ class Ability
end
def project_report_rules
- project_guest_rules + [
+ @project_report_rules ||= project_guest_rules + [
:create_commit_status,
:read_commit_statuses,
:download_code,
@@ -149,17 +170,18 @@ class Ability
end
def project_dev_rules
- project_report_rules + [
+ @project_dev_rules ||= project_report_rules + [
:admin_merge_request,
:create_merge_request,
:create_wiki,
:manage_builds,
+ :download_build_artifacts,
:push_code
]
end
def project_archived_rules
- [
+ @project_archived_rules ||= [
:create_merge_request,
:push_code,
:push_code_to_protected_branches,
@@ -169,7 +191,7 @@ class Ability
end
def project_master_rules
- project_dev_rules + [
+ @project_master_rules ||= project_dev_rules + [
:push_code_to_protected_branches,
:update_project_snippet,
:update_merge_request,
@@ -184,7 +206,7 @@ class Ability
end
def project_admin_rules
- project_master_rules + [
+ @project_admin_rules ||= project_master_rules + [
:change_namespace,
:change_visibility_level,
:rename_project,
@@ -230,18 +252,19 @@ class Ability
# Only group masters and group owners can create new projects in group
if group.has_master?(user) || group.has_owner?(user) || user.admin?
- rules.push(*[
+ rules += [
:create_projects,
- ])
+ :admin_milestones
+ ]
end
# Only group owner and administrators can admin group
if group.has_owner?(user) || user.admin?
- rules.push(*[
+ rules += [
:admin_group,
:admin_namespace,
:admin_group_member
- ])
+ ]
end
rules.flatten
@@ -252,16 +275,15 @@ class Ability
# Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin?
- rules.push(*[
+ rules += [
:create_projects,
:admin_namespace
- ])
+ ]
end
rules.flatten
end
-
[:issue, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject|
rules = []
@@ -278,7 +300,7 @@ class Ability
end
end
- [:note, :project_snippet, :personal_snippet].each do |name|
+ [:note, :project_snippet].each do |name|
define_method "#{name}_abilities" do |user, subject|
rules = []
@@ -298,19 +320,57 @@ class Ability
end
end
+ def personal_snippet_abilities(user, snippet)
+ rules = []
+
+ if snippet.author == user
+ rules += [
+ :read_personal_snippet,
+ :update_personal_snippet,
+ :admin_personal_snippet
+ ]
+ end
+
+ if snippet.public? || snippet.internal?
+ rules << :read_personal_snippet
+ end
+
+ rules
+ end
+
def group_member_abilities(user, subject)
rules = []
target_user = subject.user
group = subject.group
- can_manage = group_abilities(user, group).include?(:admin_group_member)
- if can_manage && (user != target_user)
- rules << :update_group_member
- rules << :destroy_group_member
+ unless group.last_owner?(target_user)
+ can_manage = group_abilities(user, group).include?(:admin_group_member)
+
+ if can_manage
+ rules << :update_group_member
+ rules << :destroy_group_member
+ elsif user == target_user
+ rules << :destroy_group_member
+ end
end
- if !group.last_owner?(user) && (can_manage || (user == target_user))
- rules << :destroy_group_member
+ rules
+ end
+
+ def project_member_abilities(user, subject)
+ rules = []
+ target_user = subject.user
+ project = subject.project
+
+ unless target_user == project.owner
+ can_manage = project_abilities(user, project).include?(:admin_project_member)
+
+ if can_manage
+ rules << :update_project_member
+ rules << :destroy_project_member
+ elsif user == target_user
+ rules << :destroy_project_member
+ end
end
rules
@@ -318,10 +378,10 @@ class Ability
def abilities
@abilities ||= begin
- abilities = Six.new
- abilities << self
- abilities
- end
+ abilities = Six.new
+ abilities << self
+ abilities
+ end
end
private
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 89b3116b9f2..55864236b2f 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -18,4 +18,10 @@ class AbuseReport < ActiveRecord::Base
validates :user, presence: true
validates :message, presence: true
validates :user_id, uniqueness: true
+
+ def notify
+ return unless self.persisted?
+
+ AbuseReportMailer.notify(self.id).deliver_later
+ end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 05430c2ee18..be69d317d73 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -2,51 +2,74 @@
#
# Table name: application_settings
#
-# id :integer not null, primary key
-# default_projects_limit :integer
-# signup_enabled :boolean
-# signin_enabled :boolean
-# gravatar_enabled :boolean
-# sign_in_text :text
-# created_at :datetime
-# updated_at :datetime
-# home_page_url :string(255)
-# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
-# restricted_visibility_levels :text
-# version_check_enabled :boolean default(TRUE)
-# max_attachment_size :integer default(10), not null
-# default_project_visibility :integer
-# default_snippet_visibility :integer
-# restricted_signup_domains :text
-# user_oauth_applications :boolean default(TRUE)
-# after_sign_out_path :string(255)
-# session_expire_delay :integer default(10080), not null
-# import_sources :text
+# id :integer not null, primary key
+# default_projects_limit :integer
+# signup_enabled :boolean
+# signin_enabled :boolean
+# gravatar_enabled :boolean
+# sign_in_text :text
+# created_at :datetime
+# updated_at :datetime
+# home_page_url :string(255)
+# default_branch_protection :integer default(2)
+# twitter_sharing_enabled :boolean default(TRUE)
+# restricted_visibility_levels :text
+# version_check_enabled :boolean default(TRUE)
+# max_attachment_size :integer default(10), not null
+# default_project_visibility :integer
+# default_snippet_visibility :integer
+# restricted_signup_domains :text
+# user_oauth_applications :boolean default(TRUE)
+# after_sign_out_path :string(255)
+# session_expire_delay :integer default(10080), not null
+# import_sources :text
+# help_page_text :text
+# admin_notification_email :string(255)
+# shared_runners_enabled :boolean default(TRUE), not null
+# max_artifacts_size :integer default(100), not null
+# runners_registration_token :string(255)
+# require_two_factor_authentication :boolean default(TRUE)
+# two_factor_grace_period :integer default(48)
#
class ApplicationSetting < ActiveRecord::Base
+ include TokenAuthenticatable
+ add_authentication_token_field :runners_registration_token
+
+ CACHE_KEY = 'application_setting.last'
+
serialize :restricted_visibility_levels
serialize :import_sources
serialize :restricted_signup_domains, Array
attr_accessor :restricted_signup_domains_raw
validates :session_expire_delay,
- presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :home_page_url,
- allow_blank: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" },
- if: :home_page_url_column_exist
+ allow_blank: true,
+ url: true,
+ if: :home_page_url_column_exist
validates :after_sign_out_path,
- allow_blank: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
+ allow_blank: true,
+ url: true
validates :admin_notification_email,
- allow_blank: true,
- email: true
+ allow_blank: true,
+ email: true
+
+ validates :two_factor_grace_period,
+ numericality: { greater_than_or_equal_to: 0 }
+
+ validates :recaptcha_site_key,
+ presence: true,
+ if: :recaptcha_enabled
+
+ validates :recaptcha_private_key,
+ presence: true,
+ if: :recaptcha_enabled
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
@@ -68,8 +91,20 @@ class ApplicationSetting < ActiveRecord::Base
end
end
+ before_save :ensure_runners_registration_token
+
+ after_commit do
+ Rails.cache.write(CACHE_KEY, self)
+ end
+
def self.current
- ApplicationSetting.last
+ Rails.cache.fetch(CACHE_KEY) do
+ ApplicationSetting.last
+ end
+ end
+
+ def self.expire
+ Rails.cache.delete(CACHE_KEY)
end
def self.create_from_defaults
@@ -87,7 +122,11 @@ class ApplicationSetting < ActiveRecord::Base
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: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git'],
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ max_artifacts_size: Settings.artifacts['max_size'],
+ require_two_factor_authentication: false,
+ two_factor_grace_period: 48
)
end
@@ -102,13 +141,16 @@ class ApplicationSetting < ActiveRecord::Base
def restricted_signup_domains_raw=(values)
self.restricted_signup_domains = []
self.restricted_signup_domains = values.split(
- /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
- | # or
- \s # any whitespace character
- | # or
- [\r\n] # any number of newline characters
- /x)
+ /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
+ | # or
+ \s # any whitespace character
+ | # or
+ [\r\n] # any number of newline characters
+ /x)
self.restricted_signup_domains.reject! { |d| d.empty? }
end
+ def runners_registration_token
+ ensure_runners_registration_token!
+ end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 05f5e979695..ad514706160 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -16,12 +16,12 @@
class BroadcastMessage < ActiveRecord::Base
include Sortable
- validates :message, presence: true
+ validates :message, presence: true
validates :starts_at, presence: true
- validates :ends_at, presence: true
+ validates :ends_at, presence: true
- validates :color, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true
- validates :font, format: { with: /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/ }, allow_blank: true
+ validates :color, allow_blank: true, color: true
+ validates :font, allow_blank: true, color: true
def self.current
where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb
deleted file mode 100644
index 0cf496f7d81..00000000000
--- a/app/models/ci/application_setting.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# == Schema Information
-#
-# Table name: application_settings
-#
-# id :integer not null, primary key
-# all_broken_builds :boolean
-# add_pusher :boolean
-# created_at :datetime
-# updated_at :datetime
-#
-
-module Ci
- class ApplicationSetting < ActiveRecord::Base
- extend Ci::Model
-
- def self.current
- Ci::ApplicationSetting.last
- end
-
- def self.create_from_defaults
- create(
- all_broken_builds: Settings.gitlab_ci['all_broken_builds'],
- add_pusher: Settings.gitlab_ci['add_pusher'],
- )
- end
- end
-end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index b19e2ac1363..d7fccb2197d 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: builds
+# Table name: ci_builds
#
# id :integer not null, primary key
# project_id :integer
@@ -11,16 +11,24 @@
# updated_at :datetime
# started_at :datetime
# runner_id :integer
-# commit_id :integer
# coverage :float
+# commit_id :integer
# commands :text
# job_id :integer
# name :string(255)
+# deploy :boolean default(FALSE)
# options :text
# allow_failure :boolean default(FALSE), not null
# stage :string(255)
-# deploy :boolean default(FALSE)
# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
#
module Ci
@@ -39,11 +47,15 @@ module Ci
scope :ignore_failures, ->() { where(allow_failure: false) }
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
+ mount_uploader :artifacts_file, ArtifactUploader
+
acts_as_taggable
# To prevent db load megabytes of data from trace
default_scope -> { select(Ci::Build.columns_without_lazy) }
+ before_destroy { project }
+
class << self
def columns_without_lazy
(column_names - LAZY_ATTRIBUTES).map do |column_name|
@@ -74,6 +86,7 @@ module Ci
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.name = build.name
new_build.allow_failure = build.allow_failure
@@ -86,19 +99,16 @@ module Ci
end
state_machine :status, initial: :pending do
- after_transition any => [:success, :failed, :canceled] do |build, transition|
- project = build.project
+ after_transition pending: :running do |build, transition|
+ build.execute_hooks
+ end
- if project.web_hooks?
- Ci::WebHookService.new.build_end(build)
- end
+ after_transition any => [:success, :failed, :canceled] do |build, transition|
+ return unless build.project
+ build.update_coverage
build.commit.create_next_builds(build)
- project.execute_services(build)
-
- if project.coverage_enabled?
- build.update_coverage
- end
+ build.execute_hooks
end
end
@@ -106,21 +116,35 @@ module Ci
failed? && allow_failure?
end
+ def retryable?
+ project.builds_enabled? && commands.present?
+ end
+
+ def retried?
+ !self.commit.latest_builds_for_ref(self.ref).include?(self)
+ end
+
def trace_html
html = Ci::Ansi2html::convert(trace) if trace.present?
html || ''
end
def timeout
- project.timeout
+ project.build_timeout
end
def variables
predefined_variables + yaml_variables + project_variables + trigger_variables
end
- def project
- commit.project
+ def merge_request
+ merge_requests = MergeRequest.includes(:merge_request_diff)
+ .where(source_branch: ref, source_project_id: commit.gl_project_id)
+ .reorder(iid: :asc)
+
+ merge_requests.find do |merge_request|
+ merge_request.commits.any? { |ci| ci.id == commit.sha }
+ end
end
def project_id
@@ -131,26 +155,21 @@ module Ci
project.name
end
- def project_recipients
- recipients = project.email_recipients.split(' ')
-
- if project.email_add_pusher? && user.present? && user.notification_email.present?
- recipients << user.notification_email
- end
-
- recipients.uniq
- end
-
def repo_url
- project.repo_url_with_auth
+ auth = "gitlab-ci-token:#{token}@"
+ project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
+ prefix + auth
+ end
end
def allow_git_fetch
- project.allow_git_fetch
+ project.build_allow_git_fetch
end
def update_coverage
- coverage = extract_coverage(trace, project.coverage_regex)
+ coverage_regex = project.build_coverage_regex
+ return unless coverage_regex
+ coverage = extract_coverage(trace, coverage_regex)
if coverage.is_a? Numeric
update_attributes(coverage: coverage)
@@ -159,7 +178,8 @@ module Ci
def extract_coverage(text, regex)
begin
- matches = text.gsub(Regexp.new(regex)).to_a.last
+ matches = text.scan(Regexp.new(regex)).last
+ matches = matches.last if matches.kind_of?(Array)
coverage = matches.gsub(/\d+(\.\d+)?/).first
if coverage.present?
@@ -172,8 +192,11 @@ module Ci
end
def raw_trace
- if File.exist?(path_to_trace)
+ if File.file?(path_to_trace)
File.read(path_to_trace)
+ elsif project.ci_id && File.file?(old_path_to_trace)
+ # Temporary fix for build trace data integrity
+ File.read(old_path_to_trace)
else
# backward compatibility
read_attribute :trace
@@ -183,15 +206,15 @@ module Ci
def trace
trace = raw_trace
if project && trace.present?
- trace.gsub(project.token, 'xxxxxx')
+ trace.gsub(project.runners_token, 'xxxxxx')
else
trace
end
end
def trace=(trace)
- unless Dir.exists? dir_to_trace
- FileUtils.mkdir_p dir_to_trace
+ unless Dir.exists?(dir_to_trace)
+ FileUtils.mkdir_p(dir_to_trace)
end
File.write(path_to_trace, trace)
@@ -209,22 +232,79 @@ module Ci
"#{dir_to_trace}/#{id}.log"
end
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ # Should be removed in 8.4, after CI files migration has been done.
+ #
+ def old_dir_to_trace
+ File.join(
+ Settings.gitlab_ci.builds_path,
+ created_at.utc.strftime("%Y_%m"),
+ project.ci_id.to_s
+ )
+ end
+
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ # Should be removed in 8.4, after CI files migration has been done.
+ #
+ def old_path_to_trace
+ "#{old_dir_to_trace}/#{id}.log"
+ end
+
+ ##
+ # Deprecated
+ #
+ # This contains a hotfix for CI build data integrity, see #4246
+ #
+ # This method is used by `ArtifactUploader` to create a store_dir.
+ # Warning: Uploader uses it after AND before file has been stored.
+ #
+ # This method returns old path to artifacts only if it already exists.
+ #
+ def artifacts_path
+ old = File.join(created_at.utc.strftime('%Y_%m'),
+ project.ci_id.to_s,
+ id.to_s)
+
+ old_store = File.join(ArtifactUploader.artifacts_path, old)
+ return old if project.ci_id && File.directory?(old_store)
+
+ File.join(
+ created_at.utc.strftime('%Y_%m'),
+ project.id.to_s,
+ id.to_s
+ )
+ end
+
+ def token
+ project.runners_token
+ end
+
+ def valid_token? token
+ project.valid_runners_token? token
+ end
+
def target_url
Gitlab::Application.routes.url_helpers.
- namespace_project_build_url(gl_project.namespace, gl_project, self)
+ namespace_project_build_url(project.namespace, project, self)
end
def cancel_url
if active?
Gitlab::Application.routes.url_helpers.
- cancel_namespace_project_build_path(gl_project.namespace, gl_project, self)
+ cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
- if commands.present?
+ if retryable?
Gitlab::Application.routes.url_helpers.
- retry_namespace_project_build_path(gl_project.namespace, gl_project, self)
+ retry_namespace_project_build_path(project.namespace, project, self)
end
end
@@ -240,6 +320,21 @@ module Ci
pending? && !any_runners_online?
end
+ def download_url
+ if artifacts_file.exists?
+ Gitlab::Application.routes.url_helpers.
+ download_namespace_project_build_path(project.namespace, project, self)
+ end
+ end
+
+ def execute_hooks
+ build_data = Gitlab::BuildDataBuilder.build(self)
+ project.execute_hooks(build_data.dup, :build_hooks)
+ project.execute_services(build_data.dup, :build_hooks)
+ end
+
+
+
private
def yaml_variables
diff --git a/app/models/ci/commit.rb b/app/models/ci/commit.rb
index 13437b2483f..d2a29236942 100644
--- a/app/models/ci/commit.rb
+++ b/app/models/ci/commit.rb
@@ -1,26 +1,27 @@
# == Schema Information
#
-# Table name: commits
+# Table name: ci_commits
#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+# gl_project_id :integer
#
module Ci
class Commit < ActiveRecord::Base
extend Ci::Model
- belongs_to :gl_project, class_name: '::Project', foreign_key: :gl_project_id
- has_many :statuses, dependent: :destroy, class_name: 'CommitStatus'
+ 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'
@@ -37,10 +38,6 @@ module Ci
sha
end
- def project
- @project ||= gl_project.ensure_gitlab_ci_project
- end
-
def project_id
project.id
end
@@ -56,7 +53,7 @@ module Ci
end
def valid_commit_sha
- if self.sha == Ci::Git::BLANK_SHA
+ if self.sha == Gitlab::Git::BLANK_SHA
self.errors.add(:sha, " cant be 00000000 (branch removal)")
end
end
@@ -78,7 +75,7 @@ module Ci
end
def commit_data
- @commit ||= gl_project.commit(sha)
+ @commit ||= project.commit(sha)
rescue
nil
end
@@ -164,21 +161,31 @@ module Ci
status == 'canceled'
end
+ def active?
+ running? || pending?
+ end
+
+ def complete?
+ canceled? || success? || failed?
+ end
+
def duration
duration_array = latest_statuses.map(&:duration).compact
duration_array.reduce(:+).to_i
end
+ def started_at
+ @started_at ||= statuses.order('started_at ASC').first.try(:started_at)
+ end
+
def finished_at
@finished_at ||= statuses.order('finished_at DESC').first.try(:finished_at)
end
def coverage
- if project.coverage_enabled?
- coverage_array = latest_builds.map(&:coverage).compact
- if coverage_array.size >= 1
- '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
- end
+ coverage_array = latest_builds.map(&:coverage).compact
+ if coverage_array.size >= 1
+ '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
end
end
@@ -187,18 +194,18 @@ module Ci
end
def config_processor
- @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file)
- rescue Ci::GitlabCiYamlProcessor::ValidationError => e
+ return nil unless ci_yaml_file
+ @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
+ rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
save_yaml_error(e.message)
nil
- rescue Exception => e
- logger.error e.message + "\n" + e.backtrace.join("\n")
- save_yaml_error("Undefined yaml error")
+ rescue
+ save_yaml_error("Undefined error")
nil
end
def ci_yaml_file
- gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data
+ @ci_yaml_file ||= project.repository.blob_at(sha, '.gitlab-ci.yml').data
rescue
nil
end
diff --git a/app/models/ci/event.rb b/app/models/ci/event.rb
deleted file mode 100644
index cac3a7a49c1..00000000000
--- a/app/models/ci/event.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# == Schema Information
-#
-# Table name: events
-#
-# id :integer not null, primary key
-# project_id :integer
-# user_id :integer
-# is_admin :integer
-# description :text
-# created_at :datetime
-# updated_at :datetime
-#
-
-module Ci
- class Event < ActiveRecord::Base
- extend Ci::Model
-
- belongs_to :project, class_name: 'Ci::Project'
-
- validates :description,
- presence: true,
- length: { in: 5..200 }
-
- scope :admin, ->(){ where(is_admin: true) }
- scope :project_wide, ->(){ where(is_admin: false) }
- end
-end
diff --git a/app/models/ci/project.rb b/app/models/ci/project.rb
deleted file mode 100644
index eb65c773570..00000000000
--- a/app/models/ci/project.rb
+++ /dev/null
@@ -1,215 +0,0 @@
-# == Schema Information
-#
-# Table name: projects
-#
-# id :integer not null, primary key
-# name :string(255) not null
-# timeout :integer default(3600), not null
-# created_at :datetime
-# updated_at :datetime
-# token :string(255)
-# default_ref :string(255)
-# path :string(255)
-# always_build :boolean default(FALSE), not null
-# polling_interval :integer
-# public :boolean default(FALSE), not null
-# ssh_url_to_repo :string(255)
-# gitlab_id :integer
-# allow_git_fetch :boolean default(TRUE), not null
-# email_recipients :string(255) default(""), not null
-# email_add_pusher :boolean default(TRUE), not null
-# email_only_broken_builds :boolean default(TRUE), not null
-# skip_refs :string(255)
-# coverage_regex :string(255)
-# shared_runners_enabled :boolean default(FALSE)
-# generated_yaml_config :text
-#
-
-module Ci
- class Project < ActiveRecord::Base
- extend Ci::Model
-
- include Ci::ProjectStatus
-
- belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id
-
- has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
- has_many :runners, through: :runner_projects, class_name: 'Ci::Runner'
- has_many :web_hooks, dependent: :destroy, class_name: 'Ci::WebHook'
- has_many :events, dependent: :destroy, class_name: 'Ci::Event'
- has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
- has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
-
- # Project services
- has_many :services, dependent: :destroy, class_name: 'Ci::Service'
- has_one :hip_chat_service, dependent: :destroy, class_name: 'Ci::HipChatService'
- has_one :slack_service, dependent: :destroy, class_name: 'Ci::SlackService'
- has_one :mail_service, dependent: :destroy, class_name: 'Ci::MailService'
-
- accepts_nested_attributes_for :variables, allow_destroy: true
-
- delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project
-
- #
- # Validations
- #
- validates_presence_of :timeout, :token, :default_ref, :gitlab_id
-
- validates_uniqueness_of :gitlab_id
-
- validates :polling_interval,
- presence: true,
- if: ->(project) { project.always_build.present? }
-
- before_validation :set_default_values
-
- class << self
- include Ci::CurrentSettings
-
- def base_build_script
- <<-eos
- git submodule update --init
- ls -la
- eos
- end
-
- def parse(project)
- params = {
- gitlab_id: project.id,
- default_ref: project.default_branch || 'master',
- email_add_pusher: current_application_settings.add_pusher,
- email_only_broken_builds: current_application_settings.all_broken_builds,
- }
-
- project = Ci::Project.new(params)
- project.build_missing_services
- project
- end
-
- def already_added?(project)
- where(gitlab_id: project.id).any?
- end
-
- def unassigned(runner)
- joins("LEFT JOIN #{Ci::RunnerProject.table_name} ON #{Ci::RunnerProject.table_name}.project_id = #{Ci::Project.table_name}.id " \
- "AND #{Ci::RunnerProject.table_name}.runner_id = #{runner.id}").
- where("#{Ci::RunnerProject.table_name}.project_id" => nil)
- end
-
- def ordered_by_last_commit_date
- last_commit_subquery = "(SELECT gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)"
- joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id").
- order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
- end
- end
-
- def name
- name_with_namespace
- end
-
- def path
- path_with_namespace
- end
-
- def gitlab_url
- web_url
- end
-
- def any_runners?(&block)
- if runners.active.any?(&block)
- return true
- end
-
- shared_runners_enabled && Ci::Runner.shared.active.any?(&block)
- end
-
- def set_default_values
- self.token = SecureRandom.hex(15) if self.token.blank?
- self.default_ref ||= 'master'
- end
-
- def tracked_refs
- @tracked_refs ||= default_ref.split(",").map { |ref| ref.strip }
- end
-
- def valid_token? token
- self.token && self.token == token
- end
-
- def no_running_builds?
- # Get running builds not later than 3 days ago to ignore hangs
- builds.running.where("updated_at > ?", 3.days.ago).empty?
- end
-
- def email_notification?
- email_add_pusher || email_recipients.present?
- end
-
- def web_hooks?
- web_hooks.any?
- end
-
- def services?
- services.any?
- end
-
- def timeout_in_minutes
- timeout / 60
- end
-
- def timeout_in_minutes=(value)
- self.timeout = value.to_i * 60
- end
-
- def coverage_enabled?
- coverage_regex.present?
- end
-
- # Build a clone-able repo url
- # using http and basic auth
- def repo_url_with_auth
- auth = "gitlab-ci-token:#{token}@"
- http_url_to_repo.sub(/^https?:\/\//) do |prefix|
- prefix + auth
- end
- end
-
- def available_services_names
- %w(slack mail hip_chat)
- end
-
- def build_missing_services
- available_services_names.each do |service_name|
- service = services.find { |service| service.to_param == service_name }
-
- # If service is available but missing in db
- # we should create an instance. Ex `create_gitlab_ci_service`
- self.send :"create_#{service_name}_service" if service.nil?
- end
- end
-
- def execute_services(data)
- services.each do |service|
-
- # Call service hook only if it is active
- begin
- service.execute(data) if service.active && service.can_execute?(data)
- rescue => e
- logger.error(e)
- end
- end
- end
-
- def setup_finished?
- commits.any?
- end
-
- def commits
- gl_project.ci_commits.ordered
- end
-
- def builds
- gl_project.ci_builds
- end
- end
-end
diff --git a/app/models/ci/project_status.rb b/app/models/ci/project_status.rb
deleted file mode 100644
index b66f1212f23..00000000000
--- a/app/models/ci/project_status.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Ci
- module ProjectStatus
- def status
- last_commit.status if last_commit
- end
-
- def broken?
- last_commit.failed? if last_commit
- end
-
- def success?
- last_commit.success? if last_commit
- end
-
- def broken_or_success?
- broken? || success?
- end
-
- def last_commit
- @last_commit ||= commits.last if commits.any?
- end
-
- def last_commit_date
- last_commit.try(:created_at)
- end
-
- def human_status
- status
- end
-
- def last_commit_for_ref(ref)
- commits.where(ref: ref).last
- end
- end
-end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 1b3669f1b7a..38b20cd7faa 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runners
+# Table name: ci_runners
#
# id :integer not null, primary key
# token :string(255)
@@ -25,7 +25,7 @@ module Ci
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
- has_many :projects, through: :runner_projects, class_name: 'Ci::Project'
+ has_many :projects, through: :runner_projects, class_name: '::Project', foreign_key: :gl_project_id
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
@@ -36,6 +36,7 @@ module Ci
scope :active, ->() { where(active: true) }
scope :paused, ->() { where(active: false) }
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
+ scope :ordered, ->() { order(id: :desc) }
acts_as_taggable
@@ -44,10 +45,6 @@ module Ci
query: "%#{query.try(:downcase)}%")
end
- def gl_projects_ids
- projects.select(:gitlab_id)
- end
-
def set_default_values
self.token = SecureRandom.hex(15) if self.token.blank?
end
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 44453ee4b41..93d9be144e8 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runner_projects
+# Table name: ci_runner_projects
#
# id :integer not null, primary key
# runner_id :integer not null
@@ -14,8 +14,8 @@ module Ci
extend Ci::Model
belongs_to :runner, class_name: 'Ci::Runner'
- belongs_to :project, class_name: 'Ci::Project'
+ belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- validates_uniqueness_of :runner_id, scope: :project_id
+ validates_uniqueness_of :runner_id, scope: :gl_project_id
end
end
diff --git a/app/models/ci/service.rb b/app/models/ci/service.rb
deleted file mode 100644
index ed5e3f940b6..00000000000
--- a/app/models/ci/service.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-# To add new service you should build a class inherited from Service
-# and implement a set of methods
-module Ci
- class Service < ActiveRecord::Base
- extend Ci::Model
-
- serialize :properties, JSON
-
- default_value_for :active, false
-
- after_initialize :initialize_properties
-
- belongs_to :project, class_name: 'Ci::Project'
-
- validates :project_id, presence: true
-
- def activated?
- active
- end
-
- def category
- :common
- end
-
- def initialize_properties
- self.properties = {} if properties.nil?
- end
-
- def title
- # implement inside child
- end
-
- def description
- # implement inside child
- end
-
- def help
- # implement inside child
- end
-
- def to_param
- # implement inside child
- end
-
- def fields
- # implement inside child
- []
- end
-
- def can_test?
- project.builds.any?
- end
-
- def can_execute?(build)
- true
- end
-
- def execute(build)
- # implement inside child
- end
-
- # Provide convenient accessor methods
- # for each serialized property.
- def self.prop_accessor(*args)
- args.each do |arg|
- class_eval %{
- def #{arg}
- (properties || {})['#{arg}']
- end
-
- def #{arg}=(value)
- self.properties ||= {}
- self.properties['#{arg}'] = value
- end
- }
- end
- end
-
- def self.boolean_accessor(*args)
- self.prop_accessor(*args)
-
- args.each do |arg|
- class_eval %{
- def #{arg}?
- ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
- end
- }
- end
- end
- end
-end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index fe224b7dc70..23516709a41 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: triggers
+# Table name: ci_triggers
#
# id :integer not null, primary key
# token :string(255)
@@ -16,7 +16,7 @@ module Ci
acts_as_paranoid
- belongs_to :project, class_name: 'Ci::Project'
+ belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
validates_presence_of :token
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 29cd9553394..9973d2e5ade 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: trigger_requests
+# Table name: ci_trigger_requests
#
# id :integer not null, primary key
# trigger_id :integer not null
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7a542802fa6..56759d3e50f 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: variables
+# Table name: ci_variables
#
# id :integer not null, primary key
# project_id :integer not null
@@ -15,10 +15,10 @@ module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
- belongs_to :project, class_name: 'Ci::Project'
+ belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
validates_presence_of :key
- validates_uniqueness_of :key, scope: :project_id
+ validates_uniqueness_of :key, scope: :gl_project_id
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
diff --git a/app/models/ci/web_hook.rb b/app/models/ci/web_hook.rb
deleted file mode 100644
index 8f03b0625da..00000000000
--- a/app/models/ci/web_hook.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-# id :integer not null, primary key
-# url :string(255) not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-#
-
-module Ci
- class WebHook < ActiveRecord::Base
- extend Ci::Model
-
- include HTTParty
-
- belongs_to :project, class_name: 'Ci::Project'
-
- # HTTParty timeout
- default_timeout 10
-
- validates :url, presence: true,
- format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
-
- def execute(data)
- parsed_url = URI.parse(url)
- if parsed_url.userinfo.blank?
- Ci::WebHook.post(url, body: data.to_json, headers: { "Content-Type" => "application/json" }, verify: false)
- else
- post_url = url.gsub("#{parsed_url.userinfo}@", "")
- auth = {
- username: URI.decode(parsed_url.user),
- password: URI.decode(parsed_url.password),
- }
- Ci::WebHook.post(post_url,
- body: data.to_json,
- headers: { "Content-Type" => "application/json" },
- verify: false,
- basic_auth: auth)
- end
- end
- end
-end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 492f6be1ce3..0ba7b584d91 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -7,7 +7,7 @@ class Commit
include Referable
include StaticModel
- attr_mentionable :safe_message
+ attr_mentionable :safe_message, pipeline: :single_line
participant :author, :committer, :notes
attr_accessor :project
@@ -78,11 +78,23 @@ class Commit
}x
end
+ def self.link_reference_pattern
+ super("commit", /(?<commit>\h{6,40})/)
+ end
+
def to_reference(from_project = nil)
if cross_project_reference?(from_project)
- "#{project.to_reference}@#{id}"
+ project.to_reference + self.class.reference_prefix + self.id
else
- id
+ self.id
+ end
+ end
+
+ def reference_link_text(from_project = nil)
+ if cross_project_reference?(from_project)
+ project.to_reference + self.class.reference_prefix + self.short_id
+ else
+ self.short_id
end
end
@@ -135,10 +147,10 @@ class Commit
description.present?
end
- def hook_attrs
+ def hook_attrs(with_changed_files: false)
path_with_namespace = project.path_with_namespace
- {
+ data = {
id: id,
message: safe_message,
timestamp: committed_date.xmlschema,
@@ -148,6 +160,12 @@ class Commit
email: author_email
}
}
+
+ if with_changed_files
+ data.merge!(repo_changes)
+ end
+
+ data
end
# Discover issues should be closed when this commit is pushed to a project's
@@ -157,11 +175,11 @@ class Commit
end
def author
- @author ||= User.find_by_any_email(author_email)
+ @author ||= User.find_by_any_email(author_email.downcase)
end
def committer
- @committer ||= User.find_by_any_email(committer_email)
+ @committer ||= User.find_by_any_email(committer_email.downcase)
end
def parents
@@ -196,4 +214,22 @@ class Commit
def status
ci_commit.try(:status) || :not_found
end
+
+ private
+
+ def repo_changes
+ changes = { added: [], modified: [], removed: [] }
+
+ diffs.each do |diff|
+ if diff.deleted_file
+ changes[:removed] << diff.old_path
+ elsif diff.renamed_file || diff.new_file
+ changes[:added] << diff.new_path
+ else
+ changes[:modified] << diff.new_path
+ end
+ end
+
+ changes
+ end
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 86fc9eb01a3..14e7971fa06 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -2,36 +2,38 @@
#
# Examples:
#
-# range = CommitRange.new('f3f85602...e86e1013')
+# range = CommitRange.new('f3f85602...e86e1013', project)
# range.exclude_start? # => false
# range.reference_title # => "Commits f3f85602 through e86e1013"
# range.to_s # => "f3f85602...e86e1013"
#
-# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae')
+# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae', project)
# range.exclude_start? # => true
# range.reference_title # => "Commits f3f85602^ through e86e1013"
# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
# range.to_s # => "f3f85602..e86e1013"
#
-# # Assuming `project` is a Project with a repository containing both commits:
-# range.project = project
+# # Assuming the specified project has a repository containing both commits:
# range.valid_commits? # => true
#
class CommitRange
include ActiveModel::Conversion
include Referable
- attr_reader :sha_from, :notation, :sha_to
+ attr_reader :commit_from, :notation, :commit_to
+ attr_reader :ref_from, :ref_to
# Optional Project model
attr_accessor :project
- # See `exclude_start?`
- attr_reader :exclude_start
-
- # The beginning and ending SHAs can be between 6 and 40 hex characters, and
+ # The beginning and ending refs can be named or SHAs, and
# the range notation can be double- or triple-dot.
- PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
+ REF_PATTERN = /[0-9a-zA-Z][0-9a-zA-Z_.-]*[0-9a-zA-Z\^]/
+ PATTERN = /#{REF_PATTERN}\.{2,3}#{REF_PATTERN}/
+
+ # In text references, the beginning and ending refs can only be SHAs
+ # between 6 and 40 hex characters.
+ STRICT_PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
def self.reference_prefix
'@'
@@ -43,27 +45,40 @@ class CommitRange
def self.reference_pattern
%r{
(?:#{Project.reference_pattern}#{reference_prefix})?
- (?<commit_range>#{PATTERN})
+ (?<commit_range>#{STRICT_PATTERN})
}x
end
+ def self.link_reference_pattern
+ super("compare", /(?<commit_range>#{PATTERN})/)
+ end
+
# Initialize a CommitRange
#
# range_string - The String commit range.
# project - An optional Project model.
#
# Raises ArgumentError if `range_string` does not match `PATTERN`.
- def initialize(range_string, project = nil)
+ def initialize(range_string, project)
+ @project = project
+
range_string.strip!
- unless range_string.match(/\A#{PATTERN}\z/)
+ unless range_string =~ /\A#{PATTERN}\z/
raise ArgumentError, "invalid CommitRange string format: #{range_string}"
end
- @exclude_start = !range_string.include?('...')
- @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2)
+ @ref_from, @notation, @ref_to = range_string.split(/(\.{2,3})/, 2)
- @project = project
+ if project.valid_repo?
+ @commit_from = project.commit(@ref_from)
+ @commit_to = project.commit(@ref_to)
+ end
+
+ if valid_commits?
+ @ref_from = Commit.truncate_sha(sha_from) if sha_from.start_with?(@ref_from)
+ @ref_to = Commit.truncate_sha(sha_to) if sha_to.start_with?(@ref_to)
+ end
end
def inspect
@@ -71,15 +86,24 @@ class CommitRange
end
def to_s
- "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}"
+ sha_from + notation + sha_to
end
+ alias_method :id, :to_s
+
def to_reference(from_project = nil)
- # Not using to_s because we want the full SHAs
- reference = sha_from + notation + sha_to
+ if cross_project_reference?(from_project)
+ project.to_reference + self.class.reference_prefix + self.id
+ else
+ self.id
+ end
+ end
+
+ def reference_link_text(from_project = nil)
+ reference = ref_from + notation + ref_to
if cross_project_reference?(from_project)
- reference = project.to_reference + '@' + reference
+ reference = project.to_reference + self.class.reference_prefix + reference
end
reference
@@ -87,46 +111,58 @@ class CommitRange
# Returns a String for use in a link's title attribute
def reference_title
- "Commits #{suffixed_sha_from} through #{sha_to}"
+ "Commits #{sha_start} through #{sha_to}"
end
# Return a Hash of parameters for passing to a URL helper
#
# See `namespace_project_compare_url`
def to_param
- { from: suffixed_sha_from, to: sha_to }
+ { from: sha_start, to: sha_to }
end
def exclude_start?
- exclude_start
+ @notation == '..'
end
# Check if both the starting and ending commit IDs exist in a project's
# repository
- #
- # project - An optional Project to check (default: `project`)
- def valid_commits?(project = project)
- return nil unless project.present?
- return false unless project.valid_repo?
-
- commit_from.present? && commit_to.present?
+ def valid_commits?
+ commit_start.present? && commit_end.present?
end
def persisted?
true
end
- def commit_from
- @commit_from ||= project.repository.commit(suffixed_sha_from)
+ def sha_from
+ return nil unless @commit_from
+
+ @commit_from.id
+ end
+
+ def sha_to
+ return nil unless @commit_to
+
+ @commit_to.id
end
- def commit_to
- @commit_to ||= project.repository.commit(sha_to)
+ def sha_start
+ return nil unless sha_from
+
+ exclude_start? ? sha_from + '^' : sha_from
end
- private
+ def commit_start
+ return nil unless sha_start
- def suffixed_sha_from
- sha_from + (exclude_start? ? '^' : '')
+ if exclude_start?
+ @commit_start ||= project.commit(sha_start)
+ else
+ commit_from
+ end
end
+
+ alias_method :sha_end, :sha_to
+ alias_method :commit_end, :commit_to
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 8188ba3a28e..21c5c87bc3d 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,6 +1,36 @@
+# == Schema Information
+#
+# project_id integer
+# status string
+# finished_at datetime
+# trace text
+# created_at datetime
+# updated_at datetime
+# started_at datetime
+# runner_id integer
+# coverage float
+# commit_id integer
+# commands text
+# job_id integer
+# name string
+# deploy boolean default: false
+# options text
+# allow_failure boolean default: false, null: false
+# stage string
+# trigger_request_id integer
+# stage_idx integer
+# tag boolean
+# ref string
+# user_id integer
+# type string
+# target_url string
+# description string
+#
+
class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds'
+ belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
belongs_to :commit, class_name: 'Ci::Commit'
belongs_to :user
@@ -15,12 +45,11 @@ class CommitStatus < ActiveRecord::Base
scope :pending, -> { where(status: 'pending') }
scope :success, -> { where(status: 'success') }
scope :failed, -> { where(status: 'failed') }
- scope :running_or_pending, -> { where(status:[:running, :pending]) }
- scope :finished, -> { where(status:[:success, :failed, :canceled]) }
+ scope :running_or_pending, -> { where(status: [:running, :pending]) }
+ scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :latest, -> { where(id: unscope(:select).select('max(id)').group(:name, :ref)) }
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
- scope :running_or_pending, -> { where(status: [:running, :pending]) }
state_machine :status, initial: :pending do
event :run do
@@ -47,6 +76,10 @@ class CommitStatus < ActiveRecord::Base
build.update_attributes finished_at: Time.now
end
+ after_transition [:pending, :running] => :success do |build, transition|
+ MergeRequests::MergeWhenBuildSucceedsService.new(build.commit.project, nil).trigger(build)
+ end
+
state :pending, value: 'pending'
state :running, value: 'running'
state :failed, value: 'failed'
@@ -54,8 +87,7 @@ class CommitStatus < ActiveRecord::Base
state :canceled, value: 'canceled'
end
- delegate :sha, :short_sha, :gl_project,
- to: :commit, prefix: false
+ delegate :sha, :short_sha, to: :commit, prefix: false
# TODO: this should be removed with all references
def before_sha
@@ -93,4 +125,8 @@ class CommitStatus < ActiveRecord::Base
def show_warning?
false
end
+
+ def download_url
+ nil
+ end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5e964f04ef5..18a00f95b48 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -8,6 +8,7 @@ module Issuable
extend ActiveSupport::Concern
include Participable
include Mentionable
+ include StripAttribute
included do
belongs_to :author, class_name: "User"
@@ -24,7 +25,7 @@ module Issuable
scope :authored, ->(user) { where(author_id: user) }
scope :assigned_to, ->(u) { where(assignee_id: u.id)}
- scope :recent, -> { order("created_at DESC") }
+ scope :recent, -> { reorder(id: :desc) }
scope :assigned, -> { where("assignee_id IS NOT NULL") }
scope :unassigned, -> { where("assignee_id IS NULL") }
scope :of_projects, ->(ids) { where(project_id: ids) }
@@ -35,6 +36,9 @@ module Issuable
scope :order_milestone_due_desc, -> { joins(:milestone).reorder('milestones.due_date DESC, milestones.id DESC') }
scope :order_milestone_due_asc, -> { joins(:milestone).reorder('milestones.due_date ASC, milestones.id ASC') }
+ scope :join_project, -> { joins(:project) }
+ scope :references_project, -> { references(:project) }
+
delegate :name,
:email,
to: :author,
@@ -46,8 +50,10 @@ module Issuable
allow_nil: true,
prefix: true
- attr_mentionable :title, :description
+ attr_mentionable :title, pipeline: :single_line
+ attr_mentionable :description, cache: true
participant :author, :assignee, :notes_with_associations
+ strip_attributes :title
end
module ClassMethods
@@ -89,39 +95,12 @@ module Issuable
opened? || reopened?
end
- #
- # Votes
- #
-
- # Return the number of -1 comments (downvotes)
def downvotes
- filter_superceded_votes(notes.select(&:downvote?), notes).size
+ notes.awards.where(note: "thumbsdown").count
end
- def downvotes_in_percent
- if votes_count.zero?
- 0
- else
- 100.0 - upvotes_in_percent
- end
- end
-
- # Return the number of +1 comments (upvotes)
def upvotes
- filter_superceded_votes(notes.select(&:upvote?), notes).size
- end
-
- def upvotes_in_percent
- if votes_count.zero?
- 0
- else
- 100.0 / votes_count * upvotes
- end
- end
-
- # Return the total number of votes
- def votes_count
- upvotes + downvotes
+ notes.awards.where(note: "thumbsup").count
end
def subscribed?(user)
@@ -180,21 +159,20 @@ module Issuable
self.class.to_s.underscore
end
+ # Returns a Hash of attributes to be used for Twitter card metadata
+ def card_attributes
+ {
+ 'Author' => author.try(:name),
+ 'Assignee' => assignee.try(:name)
+ }
+ end
+
def notes_with_associations
notes.includes(:author, :project)
end
- private
-
- def filter_superceded_votes(votes, notes)
- filteredvotes = [] + votes
-
- votes.each do |vote|
- if vote.superceded?(notes)
- filteredvotes.delete(vote)
- end
- end
-
- filteredvotes
+ def updated_tasks
+ Taskable.get_updated_tasks(old_content: previous_changes['description'].first,
+ new_content: description)
end
end
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 193c91f1742..6316ee208b5 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -10,8 +10,9 @@ module Mentionable
module ClassMethods
# Indicate which attributes of the Mentionable to search for GFM references.
- def attr_mentionable(*attrs)
- mentionable_attrs.concat(attrs.map(&:to_s))
+ def attr_mentionable(attr, options = {})
+ attr = attr.to_s
+ mentionable_attrs << [attr, options]
end
# Accessor for attributes marked mentionable.
@@ -22,7 +23,7 @@ module Mentionable
included do
if self < Participable
- participant ->(current_user) { mentioned_users(current_user, load_lazy_references: false) }
+ participant ->(current_user) { mentioned_users(current_user) }
end
end
@@ -37,38 +38,46 @@ module Mentionable
"#{friendly_name} #{to_reference(from_project)}"
end
- # Construct a String that contains possible GFM references.
- def mentionable_text
- self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n")
- end
-
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
def local_reference
self
end
- def all_references(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
- ext = Gitlab::ReferenceExtractor.new(self.project, current_user, load_lazy_references: load_lazy_references)
- ext.analyze(text)
+ def all_references(current_user = self.author, text = nil)
+ ext = Gitlab::ReferenceExtractor.new(self.project, current_user, self.author)
+
+ if text
+ ext.analyze(text)
+ else
+ self.class.mentionable_attrs.each do |attr, options|
+ text = send(attr)
+ options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted?
+ ext.analyze(text, options)
+ end
+ end
+
ext
end
- def mentioned_users(current_user = nil, load_lazy_references: true)
- all_references(current_user, load_lazy_references: load_lazy_references).users
+ def mentioned_users(current_user = nil)
+ all_references(current_user).users
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
- def referenced_mentionables(current_user = self.author, text = self.mentionable_text, load_lazy_references: true)
- return [] if text.blank?
+ def referenced_mentionables(current_user = self.author, text = nil)
+ refs = all_references(current_user, text)
+ refs = (refs.issues + refs.merge_requests + refs.commits)
- refs = all_references(current_user, text, load_lazy_references: load_lazy_references)
- (refs.issues + refs.merge_requests + refs.commits) - [local_reference]
+ # We're using this method instead of Array diffing because that requires
+ # both of the object's `hash` values to be the same, which may not be the
+ # case for otherwise identical Commit objects.
+ refs.reject { |ref| ref == local_reference }
end
- # Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
- def create_cross_references!(author = self.author, without = [], text = self.mentionable_text)
+ # Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
+ def create_cross_references!(author = self.author, without = [], text = nil)
refs = referenced_mentionables(author, text)
-
+
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
# case for otherwise identical Commit objects.
@@ -106,12 +115,12 @@ module Mentionable
def detect_mentionable_changes
source = (changes.present? ? changes : previous_changes).dup
- mentionable = self.class.mentionable_attrs
+ mentionable = self.class.mentionable_attrs.map { |attr, options| attr }
# Only include changed fields that are mentionable
source.select { |key, val| mentionable.include?(key) }
end
-
+
# Determine whether or not a cross-reference Note has already been created between this Mentionable and
# the specified target.
def cross_reference_exists?(target)
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 85367f89f4f..fc6f83b918b 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -37,21 +37,22 @@ module Participable
# 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, load_lazy_references: true)
- participants = self.class.participant_attrs.flat_map do |attr|
- value =
- if attr.respond_to?(:call)
- instance_exec(current_user, &attr)
- else
- send(attr)
- end
+ 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
-
- if load_lazy_references
- participants = Gitlab::Markdown::ReferenceFilter::LazyReference.load(participants).uniq
+ participants_for(value, current_user)
+ end.compact.uniq
+ end
+ unless Gitlab::ReferenceExtractor.lazy?
participants.select! do |user|
user.can?(:read_project, project)
end
@@ -64,12 +65,12 @@ module Participable
def participants_for(value, current_user = nil)
case value
- when User, Gitlab::Markdown::ReferenceFilter::LazyReference
+ when User, Banzai::LazyReference
[value]
when Enumerable, ActiveRecord::Relation
value.flat_map { |v| participants_for(v, current_user) }
when Participable
- value.participants(current_user, load_lazy_references: false)
+ value.participants(current_user)
end
end
end
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index cced66cc1e4..ce064f675ae 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -21,6 +21,10 @@ module Referable
''
end
+ def reference_link_text(from_project = nil)
+ to_reference(from_project)
+ end
+
module ClassMethods
# The character that prefixes the actual reference identifier
#
@@ -44,6 +48,25 @@ module Referable
def reference_pattern
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
+
+ def link_reference_pattern(route, pattern)
+ %r{
+ (?<url>
+ #{Regexp.escape(Gitlab.config.gitlab.url)}
+ \/#{Project.reference_pattern}
+ \/#{Regexp.escape(route)}
+ \/#{pattern}
+ (?<path>
+ (\/[a-z0-9_=-]+)*
+ )?
+ (?<query>
+ \?[a-z0-9_=-]+
+ (&[a-z0-9_=-]+)*
+ )?
+ (?<anchor>\#[a-z0-9_-]+)?
+ )
+ }x
+ end
end
private
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 0ad2654867d..7391a77383c 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -8,12 +8,13 @@ module Sortable
included do
# By default all models should be ordered
# by created_at field starting from newest
- default_scope { order(created_at: :desc, id: :desc) }
+ default_scope { order_id_desc }
- scope :order_created_desc, -> { reorder(created_at: :desc, id: :desc) }
- scope :order_created_asc, -> { reorder(created_at: :asc, id: :asc) }
- scope :order_updated_desc, -> { reorder(updated_at: :desc, id: :desc) }
- scope :order_updated_asc, -> { reorder(updated_at: :asc, id: :asc) }
+ scope :order_id_desc, -> { reorder(id: :desc) }
+ scope :order_created_desc, -> { reorder(created_at: :desc) }
+ scope :order_created_asc, -> { reorder(created_at: :asc) }
+ scope :order_updated_desc, -> { reorder(updated_at: :desc) }
+ scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) }
end
diff --git a/app/models/concerns/strip_attribute.rb b/app/models/concerns/strip_attribute.rb
new file mode 100644
index 00000000000..8806ebe897a
--- /dev/null
+++ b/app/models/concerns/strip_attribute.rb
@@ -0,0 +1,34 @@
+# == Strip Attribute module
+#
+# Contains functionality to clean attributes before validation
+#
+# Usage:
+#
+# class Milestone < ActiveRecord::Base
+# strip_attributes :title
+# end
+#
+#
+module StripAttribute
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def strip_attributes(*attrs)
+ strip_attrs.concat(attrs)
+ end
+
+ def strip_attrs
+ @strip_attrs ||= []
+ end
+ end
+
+ included do
+ before_validation :strip_attributes
+ end
+
+ def strip_attributes
+ self.class.strip_attrs.each do |attr|
+ self[attr].strip! if self[attr] && self[attr].respond_to?(:strip!)
+ end
+ end
+end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 660e58b876d..df2a9e3e84b 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -7,14 +7,39 @@ require 'task_list/filter'
#
# Used by MergeRequest and Issue
module Taskable
+ COMPLETED = 'completed'.freeze
+ INCOMPLETE = 'incomplete'.freeze
+ ITEM_PATTERN = /
+ ^
+ (?:\s*[-+*]|(?:\d+\.))? # optional list prefix
+ \s* # optional whitespace prefix
+ (\[\s\]|\[[xX]\]) # checkbox
+ (\s.+) # followed by whitespace and some text.
+ /x
+
+ def self.get_tasks(content)
+ content.to_s.scan(ITEM_PATTERN).map do |checkbox, label|
+ # ITEM_PATTERN strips out the hyphen, but Item requires it. Rabble rabble.
+ TaskList::Item.new("- #{checkbox}", label.strip)
+ end
+ end
+
+ def self.get_updated_tasks(old_content:, new_content:)
+ old_tasks, new_tasks = get_tasks(old_content), get_tasks(new_content)
+
+ new_tasks.select.with_index do |new_task, i|
+ old_task = old_tasks[i]
+ next unless old_task
+
+ new_task.source == old_task.source && new_task.complete? != old_task.complete?
+ end
+ end
+
# Called by `TaskList::Summary`
def task_list_items
return [] if description.blank?
- @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
- # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
- TaskList::Item.new("- #{item}")
- end
+ @task_list_items ||= Taskable.get_tasks(description)
end
def tasks
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 9b88ec1cc38..885deaf78d2 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -1,31 +1,49 @@
module TokenAuthenticatable
extend ActiveSupport::Concern
- module ClassMethods
- def find_by_authentication_token(authentication_token = nil)
- if authentication_token
- where(authentication_token: authentication_token).first
- end
+ class_methods do
+ def authentication_token_fields
+ @token_fields || []
end
- end
- def ensure_authentication_token
- if authentication_token.blank?
- self.authentication_token = generate_authentication_token
- end
- end
+ private
- def reset_authentication_token!
- self.authentication_token = generate_authentication_token
- save
+ def add_authentication_token_field(token_field)
+ @token_fields = [] unless @token_fields
+ @token_fields << token_field
+
+ define_singleton_method("find_by_#{token_field}") do |token|
+ find_by(token_field => token) if token
+ end
+
+ define_method("ensure_#{token_field}") do
+ current_token = read_attribute(token_field)
+ current_token.blank? ? write_new_token(token_field) : current_token
+ end
+
+ define_method("ensure_#{token_field}!") do
+ send("reset_#{token_field}!") if read_attribute(token_field).blank?
+ read_attribute(token_field)
+ end
+
+ define_method("reset_#{token_field}!") do
+ write_new_token(token_field)
+ save!
+ end
+ end
end
private
- def generate_authentication_token
+ def write_new_token(token_field)
+ new_token = generate_token(token_field)
+ write_attribute(token_field, new_token)
+ end
+
+ def generate_token(token_field)
loop do
token = Devise.friendly_token
- break token unless self.class.unscoped.where(authentication_token: token).first
+ break token unless self.class.unscoped.find_by(token_field => token)
end
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 47600c57e35..01d008035a5 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -45,7 +45,7 @@ class Event < ActiveRecord::Base
after_create :reset_project_activity
# Scopes
- scope :recent, -> { order(created_at: :desc) }
+ scope :recent, -> { reorder(id: :desc) }
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
scope :with_associations, -> { includes(project: :namespace) }
@@ -63,6 +63,16 @@ class Event < ActiveRecord::Base
Event::PUSHED, ["MergeRequest", "Issue"],
[Event::CREATED, Event::CLOSED, Event::MERGED])
end
+
+ def latest_update_time
+ row = select(:updated_at, :project_id).reorder(id: :desc).take
+
+ row ? row.updated_at : nil
+ end
+
+ def limit_recent(limit = 20, offset = nil)
+ recent.limit(limit).offset(offset)
+ end
end
def proper?
@@ -191,7 +201,7 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
- if project.import?
+ if project.external_import?
"imported"
else
"created"
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index fa54e3540d0..12c934e2494 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -1,3 +1,36 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
class GenericCommitStatus < CommitStatus
before_validation :set_default_values
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
new file mode 100644
index 00000000000..0171f7d54b7
--- /dev/null
+++ b/app/models/global_label.rb
@@ -0,0 +1,17 @@
+class GlobalLabel
+ attr_accessor :title, :labels
+ alias_attribute :name, :title
+
+ def self.build_collection(labels)
+ labels = labels.group_by(&:title)
+
+ labels.map do |title, label|
+ new(title, label)
+ end
+ end
+
+ def initialize(title, labels)
+ @title = title
+ @labels = labels
+ end
+end
diff --git a/app/models/group_milestone.rb b/app/models/global_milestone.rb
index 91844da62e2..af1d7562ebe 100644
--- a/app/models/group_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -1,16 +1,32 @@
-class GroupMilestone
+class GlobalMilestone
attr_accessor :title, :milestones
alias_attribute :name, :title
+ def self.build_collection(milestones)
+ milestones = milestones.group_by(&:title)
+
+ milestones.map do |title, milestones|
+ new(title, milestones)
+ end
+ end
+
def initialize(title, milestones)
@title = title
@milestones = milestones
end
def safe_title
- @title.parameterize
+ @title.to_slug.normalize.to_s
end
-
+
+ def expired?
+ if due_date
+ due_date.past?
+ else
+ false
+ end
+ end
+
def projects
milestones.map { |milestone| milestone.project }
end
@@ -60,15 +76,15 @@ class GroupMilestone
end
def issues
- @group_issues ||= milestones.map(&:issues).flatten.group_by(&:state)
+ @issues ||= milestones.map(&:issues).flatten.group_by(&:state)
end
def merge_requests
- @group_merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
+ @merge_requests ||= milestones.map(&:merge_requests).flatten.group_by(&:state)
end
def participants
- @group_participants ||= milestones.map(&:participants).flatten.compact.uniq
+ @participants ||= milestones.map(&:participants).flatten.compact.uniq
end
def opened_issues
@@ -86,4 +102,29 @@ class GroupMilestone
def closed_merge_requests
merge_requests.values_at("closed", "merged", "locked").compact.flatten
end
+
+ def complete?
+ total_items_count == closed_items_count
+ end
+
+ def due_date
+ return @due_date if defined?(@due_date)
+
+ @due_date =
+ if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
+ @milestones.first.due_date
+ else
+ nil
+ end
+ end
+
+ def expires_at
+ if due_date
+ if due_date.past?
+ "expired at #{due_date.stamp("Aug 21, 2011")}"
+ else
+ "expires at #{due_date.stamp("Aug 21, 2011")}"
+ end
+ end
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 465c22d23ac..b8f2ab6ae5d 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,6 +11,7 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -19,8 +20,9 @@ require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
include Referable
-
+
has_many :group_members, dependent: :destroy, as: :source, class_name: 'GroupMember'
+ alias_method :members, :group_members
has_many :users, through: :group_members
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
@@ -47,6 +49,10 @@ class Group < Namespace
def reference_pattern
User.reference_pattern
end
+
+ def visible_to_user(user)
+ where(id: user.authorized_groups.select(:id).reorder(nil))
+ end
end
def to_reference(_from_project = nil)
@@ -109,20 +115,12 @@ class Group < Namespace
has_owner?(user) && owners.size == 1
end
- def members
- group_members
- end
-
def avatar_type
unless self.avatar.image?
self.errors.add :avatar, "only images allowed"
end
end
- def public_profile?
- projects.public_only.any?
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
deleted file mode 100644
index 0fc39cb8771..00000000000
--- a/app/models/group_label.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class GroupLabel
- attr_accessor :title, :labels
- alias_attribute :name, :title
-
- def initialize(title, labels)
- @title = title
- @labels = labels
- end
-end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index ca7066b959a..22638057773 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class ProjectHook < WebHook
@@ -24,4 +25,5 @@ class ProjectHook < WebHook
scope :issue_hooks, -> { where(issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
+ scope :build_hooks, -> { where(build_events: true) }
end
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index b55e217975f..09bb3ee52a2 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class ServiceHook < WebHook
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 6fb2d421026..2f63c59b07e 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class SystemHook < WebHook
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index a078accbdbd..40eb0e20b4b 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -2,18 +2,19 @@
#
# Table name: web_hooks
#
-# id :integer not null, primary key
-# url :string(255)
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# type :string(255) default("ProjectHook")
-# service_id :integer
-# push_events :boolean default(TRUE), not null
-# issues_events :boolean default(FALSE), not null
-# merge_requests_events :boolean default(FALSE), not null
-# tag_push_events :boolean default(FALSE)
-# note_events :boolean default(FALSE), not null
+# id :integer not null, primary key
+# url :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# type :string(255) default("ProjectHook")
+# service_id :integer
+# push_events :boolean default(TRUE), not null
+# issues_events :boolean default(FALSE), not null
+# merge_requests_events :boolean default(FALSE), not null
+# tag_push_events :boolean default(FALSE)
+# note_events :boolean default(FALSE), not null
+# enable_ssl_verification :boolean default(TRUE)
#
class WebHook < ActiveRecord::Base
@@ -25,42 +26,44 @@ class WebHook < ActiveRecord::Base
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
+ default_value_for :build_events, false
default_value_for :enable_ssl_verification, true
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
- validates :url, presence: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
+ validates :url, presence: true, url: true
def execute(data, hook_name)
parsed_url = URI.parse(url)
if parsed_url.userinfo.blank?
- WebHook.post(url,
- body: data.to_json,
- headers: {
- "Content-Type" => "application/json",
- "X-Gitlab-Event" => hook_name.singularize.titleize
- },
- verify: enable_ssl_verification)
+ response = WebHook.post(url,
+ body: data.to_json,
+ headers: {
+ "Content-Type" => "application/json",
+ "X-Gitlab-Event" => hook_name.singularize.titleize
+ },
+ verify: enable_ssl_verification)
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
username: URI.decode(parsed_url.user),
password: URI.decode(parsed_url.password),
}
- WebHook.post(post_url,
- body: data.to_json,
- headers: {
- "Content-Type" => "application/json",
- "X-Gitlab-Event" => hook_name.singularize.titleize
- },
- verify: enable_ssl_verification,
- basic_auth: auth)
+ response = WebHook.post(post_url,
+ body: data.to_json,
+ headers: {
+ "Content-Type" => "application/json",
+ "X-Gitlab-Event" => hook_name.singularize.titleize
+ },
+ verify: enable_ssl_verification,
+ basic_auth: auth)
end
- rescue SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
+
+ [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
+ rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
- false
+ [false, e.to_s]
end
def async_execute(data, hook_name)
diff --git a/app/models/identity.rb b/app/models/identity.rb
index ad60154be71..8bcdc194953 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -12,6 +12,7 @@
class Identity < ActiveRecord::Base
include Sortable
+ include CaseSensitivity
belongs_to :user
validates :provider, presence: true
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 72183108033..80ecd15077f 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -69,6 +69,10 @@ class Issue < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ super("issues", /(?<issue>\d+)/)
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -79,6 +83,14 @@ class Issue < ActiveRecord::Base
reference
end
+ def referenced_merge_requests
+ Gitlab::ReferenceExtractor.lazily do
+ [self, *notes].flat_map do |note|
+ note.all_references.merge_requests
+ end
+ end.sort_by(&:iid)
+ end
+
# Reset issue events cache
#
# Since we do cache @event we need to reset cache in special cases:
diff --git a/app/models/jira_issue.rb b/app/models/jira_issue.rb
new file mode 100644
index 00000000000..5b21aac5e43
--- /dev/null
+++ b/app/models/jira_issue.rb
@@ -0,0 +1,2 @@
+class JiraIssue < ExternalIssue
+end
diff --git a/app/models/label.rb b/app/models/label.rb
index 1bb4b5f55cf..220da10a6ab 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -8,6 +8,7 @@
# project_id :integer
# created_at :datetime
# updated_at :datetime
+# template :boolean default(FALSE)
#
class Label < ActiveRecord::Base
@@ -16,7 +17,7 @@ class Label < ActiveRecord::Base
# Requests that have no label assigned.
LabelStruct = Struct.new(:title, :name)
None = LabelStruct.new('No Label', 'No Label')
- Any = LabelStruct.new('Any', '')
+ Any = LabelStruct.new('Any Label', '')
DEFAULT_COLOR = '#428BCA'
@@ -26,9 +27,7 @@ class Label < ActiveRecord::Base
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
- validates :color,
- format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
- allow_blank: false
+ validates :color, color: true, allow_blank: false
validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
new file mode 100644
index 00000000000..86b1b7e2f99
--- /dev/null
+++ b/app/models/lfs_object.rb
@@ -0,0 +1,32 @@
+# == Schema Information
+#
+# Table name: lfs_objects
+#
+# id :integer not null, primary key
+# oid :string(255) not null
+# size :integer not null
+# created_at :datetime
+# updated_at :datetime
+# file :string(255)
+#
+
+class LfsObject < ActiveRecord::Base
+ has_many :lfs_objects_projects, dependent: :destroy
+ has_many :projects, through: :lfs_objects_projects
+
+ validates :oid, presence: true, uniqueness: true
+
+ mount_uploader :file, LfsObjectUploader
+
+ def storage_project(project)
+ if project && project.forked?
+ storage_project(project.forked_from_project)
+ else
+ project
+ end
+ end
+
+ def project_allowed_access?(project)
+ projects.exists?(storage_project(project).id)
+ end
+end
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
new file mode 100644
index 00000000000..890736bfc80
--- /dev/null
+++ b/app/models/lfs_objects_project.rb
@@ -0,0 +1,19 @@
+# == Schema Information
+#
+# Table name: lfs_objects_projects
+#
+# id :integer not null, primary key
+# lfs_object_id :integer not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+class LfsObjectsProject < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :lfs_object
+
+ validates :lfs_object_id, presence: true
+ validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
+ validates :project_id, presence: true
+end
diff --git a/app/models/member.rb b/app/models/member.rb
index cae8caa23fb..28aee2e3799 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -30,13 +30,22 @@ class Member < ActiveRecord::Base
validates :user, presence: true, unless: :invite?
validates :source, presence: true
- validates :user_id, uniqueness: { scope: [:source_type, :source_id],
+ validates :user_id, uniqueness: { scope: [:source_type, :source_id],
message: "already exists in source",
allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
- validates :invite_email, presence: { if: :invite? },
- email: { strict_mode: true, allow_nil: true },
- uniqueness: { scope: [:source_type, :source_id], allow_nil: true }
+ validates :invite_email,
+ presence: {
+ if: :invite?
+ },
+ email: {
+ strict_mode: true,
+ allow_nil: true
+ },
+ uniqueness: {
+ scope: [:source_type, :source_id],
+ allow_nil: true
+ }
scope :invite, -> { where(user_id: nil) }
scope :non_invite, -> { where("user_id IS NOT NULL") }
@@ -73,7 +82,7 @@ class Member < ActiveRecord::Base
def add_user(members, user_id, access_level, current_user = nil)
user = user_for_id(user_id)
-
+
# `user` can be either a User object or an email to be invited
if user.is_a?(User)
member = members.find_or_initialize_by(user_id: user.id)
@@ -82,10 +91,21 @@ class Member < ActiveRecord::Base
member.invite_email = user
end
- member.created_by ||= current_user
- member.access_level = access_level
+ if can_update_member?(current_user, member)
+ member.created_by ||= current_user
+ member.access_level = access_level
+
+ member.save
+ end
+ end
+
+ private
- member.save
+ def can_update_member?(current_user, member)
+ # There is no current user for bulk actions, in which case anything is allowed
+ !current_user ||
+ current_user.can?(:update_group_member, member) ||
+ current_user.can?(:update_project_member, member)
end
end
@@ -95,7 +115,7 @@ class Member < ActiveRecord::Base
def accept_invite!(new_user)
return false unless invite?
-
+
self.invite_token = nil
self.invite_accepted_at = Time.now.utc
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 21861a46a84..ac25d38eb63 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -2,24 +2,28 @@
#
# Table name: merge_requests
#
-# id :integer not null, primary key
-# target_branch :string(255) not null
-# source_branch :string(255) not null
-# source_project_id :integer not null
-# author_id :integer
-# assignee_id :integer
-# title :string(255)
-# created_at :datetime
-# updated_at :datetime
-# milestone_id :integer
-# state :string(255)
-# merge_status :string(255)
-# target_project_id :integer not null
-# iid :integer
-# description :text
-# position :integer default(0)
-# locked_at :datetime
-# updated_by_id :integer
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text (serialized to hash)
+# merge_when_build_succeeds :boolean default(false), not null
+# merge_user_id :integer
#
require Rails.root.join("app/models/commit")
@@ -34,13 +38,16 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, foreign_key: :target_project_id, class_name: "Project"
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
+ belongs_to :merge_user, class_name: "User"
has_one :merge_request_diff, dependent: :destroy
+ serialize :merge_params, Hash
+
after_create :create_merge_request_diff
after_update :update_merge_request_diff
- delegate :commits, :diffs, to: :merge_request_diff, prefix: nil
+ delegate :commits, :diffs, :diffs_no_whitespace, to: :merge_request_diff, prefix: nil
# When this attribute is true some MR validation is ignored
# It allows us to close or modify broken merge requests
@@ -120,6 +127,7 @@ class MergeRequest < ActiveRecord::Base
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
+ validates :merge_user, presence: true, if: :merge_when_build_succeeds?
validate :validate_branches
validate :validate_fork
@@ -133,6 +141,9 @@ class MergeRequest < ActiveRecord::Base
scope :closed, -> { with_state(:closed) }
scope :closed_and_merged, -> { with_states(:closed, :merged) }
+ scope :join_project, -> { joins(:target_project) }
+ scope :references_project, -> { references(:target_project) }
+
def self.reference_prefix
'!'
end
@@ -147,6 +158,10 @@ class MergeRequest < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ super("merge_requests", /(?<merge_request>\d+)/)
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -159,11 +174,11 @@ class MergeRequest < ActiveRecord::Base
def last_commit
merge_request_diff ? merge_request_diff.last_commit : compare_commits.last
- end
+ end
def first_commit
merge_request_diff ? merge_request_diff.first_commit : compare_commits.first
- end
+ end
def last_commit_short_sha
last_commit.short_id
@@ -179,9 +194,7 @@ class MergeRequest < ActiveRecord::Base
similar_mrs = similar_mrs.where('id not in (?)', self.id) if self.id
if similar_mrs.any?
errors.add :validate_branches,
- "Cannot Create: This merge request already exists: #{
- similar_mrs.pluck(:title)
- }"
+ "Cannot Create: This merge request already exists: #{similar_mrs.pluck(:title)}"
end
end
end
@@ -250,6 +263,16 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def can_cancel_merge_when_build_succeeds?(current_user)
+ can_be_merged_by?(current_user) || self.author == current_user
+ end
+
+ def can_remove_source_branch?(current_user)
+ !source_project.protected_branch?(source_branch) &&
+ !source_project.root_ref?(source_branch) &&
+ Ability.abilities.allowed?(current_user, :push_code, source_project)
+ end
+
def mr_and_commit_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
@@ -257,7 +280,7 @@ class MergeRequest < ActiveRecord::Base
Note.where(
"(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
- "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
+ "((project_id = :source_project_id OR project_id = :target_project_id) AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
mr_id: id,
commit_ids: commit_ids,
target_project_id: target_project_id,
@@ -287,7 +310,7 @@ class MergeRequest < ActiveRecord::Base
work_in_progress: work_in_progress?
}
- unless last_commit.nil?
+ if last_commit
attrs.merge!(last_commit: last_commit.hook_attrs)
end
@@ -312,7 +335,7 @@ class MergeRequest < ActiveRecord::Base
issues = commits.flat_map { |c| c.closes_issues(current_user) }
issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
closed_by_message(description))
- issues.uniq.sort_by(&:id)
+ issues.uniq(&:id)
else
[]
end
@@ -385,6 +408,16 @@ class MergeRequest < ActiveRecord::Base
message
end
+ def reset_merge_when_build_succeeds
+ return unless merge_when_build_succeeds?
+
+ self.merge_when_build_succeeds = false
+ self.merge_user = nil
+ self.merge_params = nil
+
+ self.save
+ end
+
# Return array of possible target branches
# depends on target project of MR
def target_branches
@@ -470,4 +503,12 @@ class MergeRequest < ActiveRecord::Base
unlock_mr if locked?
end
end
+
+ def ci_commit
+ @ci_commit ||= source_project.ci_commit(last_commit.id) if last_commit && source_project
+ end
+
+ def broken?
+ self.commits.blank? || branch_missing? || cannot_be_merged?
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 6575d0bc81f..c499a4b5b4c 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -19,7 +19,7 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 500
- attr_reader :commits, :diffs
+ attr_reader :commits, :diffs, :diffs_no_whitespace
belongs_to :merge_request
@@ -47,6 +47,20 @@ class MergeRequestDiff < ActiveRecord::Base
@diffs ||= (load_diffs(st_diffs) || [])
end
+ def diffs_no_whitespace
+ # Get latest sha of branch from source project
+ source_sha = merge_request.source_project.commit(source_branch).sha
+
+ compare_result = Gitlab::CompareResult.new(
+ Gitlab::Git::Compare.new(
+ merge_request.target_project.repository.raw_repository,
+ merge_request.target_branch,
+ source_sha,
+ ), { ignore_whitespace_change: true }
+ )
+ @diffs_no_whitespace ||= load_diffs(dump_commits(compare_result.diffs))
+ end
+
def commits
@commits ||= load_commits(st_commits || [])
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2ff16e2825c..d8c7536cd31 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -16,12 +16,13 @@
class Milestone < ActiveRecord::Base
# Represents a "No Milestone" state used for filtering Issues and Merge
# Requests that have no milestone assigned.
- MilestoneStruct = Struct.new(:title, :name)
- None = MilestoneStruct.new('No Milestone', 'No Milestone')
- Any = MilestoneStruct.new('Any', '')
+ MilestoneStruct = Struct.new(:title, :name, :id)
+ None = MilestoneStruct.new('No Milestone', 'No Milestone', 0)
+ Any = MilestoneStruct.new('Any Milestone', '', -1)
include InternalId
include Sortable
+ include StripAttribute
belongs_to :project
has_many :issues
@@ -35,6 +36,8 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true
validates :project, presence: true
+ strip_attributes :title
+
state_machine :state, initial: :active do
event :close do
transition active: :closed
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 5782e649f8b..adafabbec07 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,6 +11,7 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
class Namespace < ActiveRecord::Base
@@ -22,19 +23,17 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
- presence: true, uniqueness: true,
length: { within: 0..255 },
- format: { with: Gitlab::Regex.namespace_name_regex,
- message: Gitlab::Regex.namespace_name_regex_message }
+ namespace_name: true,
+ presence: true,
+ uniqueness: true
validates :description, length: { within: 0..255 }
validates :path,
- uniqueness: { case_sensitive: false },
- presence: true,
length: { within: 1..255 },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.namespace_regex,
- message: Gitlab::Regex.namespace_regex_message }
+ namespace: true,
+ presence: true,
+ uniqueness: { case_sensitive: false }
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -46,7 +45,7 @@ class Namespace < ActiveRecord::Base
class << self
def by_path(path)
- where('lower(path) = :value', value: path.downcase).first
+ find_by('lower(path) = :value', value: path.downcase)
end
# Case insensetive search for namespace by path or name
@@ -149,6 +148,6 @@ class Namespace < ActiveRecord::Base
end
def find_fork_of(project)
- projects.joins(:forked_project_link).where('forked_project_links.forked_from_project_id = ?', project.id).first
+ projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id)
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 0b3aa30abd7..3d5b663c99f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -16,6 +16,7 @@
# system :boolean default(FALSE), not null
# st_diff :text
# updated_by_id :integer
+# is_award :boolean default(FALSE), not null
#
require 'carrierwave/orm/activerecord'
@@ -28,7 +29,7 @@ class Note < ActiveRecord::Base
default_value_for :system, false
- attr_mentionable :note
+ attr_mentionable :note, cache: true, pipeline: :note
participant :author
belongs_to :project
@@ -39,17 +40,24 @@ class Note < ActiveRecord::Base
delegate :name, to: :project, prefix: true
delegate :name, :email, to: :author, prefix: true
+ before_validation :set_award!
+
validates :note, :project, presence: true
- validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: 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 :author, presence: true
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, '']) }
@@ -97,6 +105,19 @@ class Note < ActiveRecord::Base
def search(query)
where("LOWER(note) like :query", query: "%#{query.downcase}%")
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
+ end
end
def cross_reference?
@@ -288,44 +309,6 @@ class Note < ActiveRecord::Base
nil
end
- DOWNVOTES = %w(-1 :-1: :thumbsdown: :thumbs_down_sign:)
-
- # Check if the note is a downvote
- def downvote?
- votable? && note.start_with?(*DOWNVOTES)
- end
-
- UPVOTES = %w(+1 :+1: :thumbsup: :thumbs_up_sign:)
-
- # Check if the note is an upvote
- def upvote?
- votable? && note.start_with?(*UPVOTES)
- end
-
- def superceded?(notes)
- return false unless vote?
-
- notes.each do |note|
- next if note == self
-
- if note.vote? &&
- self[:author_id] == note[:author_id] &&
- self[:created_at] <= note[:created_at]
- return true
- end
- end
-
- false
- end
-
- def vote?
- upvote? || downvote?
- end
-
- def votable?
- for_issue? || (for_merge_request? && !for_diff_line?)
- end
-
# Mentionable override.
def gfm_reference(from_project = nil)
noteable.gfm_reference(from_project)
@@ -363,7 +346,43 @@ class Note < ActiveRecord::Base
read_attribute(:system)
end
+ def downvote?
+ is_award && note == "thumbsdown"
+ end
+
+ def upvote?
+ is_award && note == "thumbsup"
+ end
+
def editable?
- !system?
+ !system? && !is_award
+ 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
+ end
+
+ private
+
+ def awards_supported?
+ noteable.kind_of?(Issue) || noteable.is_a?(MergeRequest)
+ end
+
+ def contains_emoji_only?
+ note =~ /\A#{Banzai::Filter::EmojiFilter.emoji_pattern}\s?\Z/
+ end
+
+ def award_emoji_name
+ original_name = note.match(Banzai::Filter::EmojiFilter.emoji_pattern)[1]
+ AwardEmoji.normilize_emoji_name(original_name)
end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 88cd88dcb5a..b1a6cfa86af 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -28,6 +28,7 @@
# import_type :string(255)
# import_source :string(255)
# commit_count :integer default(0)
+# import_error :text
#
require 'carrierwave/orm/activerecord'
@@ -37,13 +38,13 @@ class Project < ActiveRecord::Base
include Gitlab::ConfigHelper
include Gitlab::ShellAdapter
include Gitlab::VisibilityLevel
+ include Gitlab::CurrentSettings
include Referable
include Sortable
include AfterCommitQueue
include CaseSensitivity
extend Gitlab::ConfigHelper
- extend Enumerize
UNKNOWN_IMPORT_URL = 'http://unknown.git'
@@ -51,9 +52,11 @@ class Project < ActiveRecord::Base
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
+ default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :wall_enabled, false
default_value_for :snippets_enabled, gitlab_config_features.snippets
+ default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
# set last_activity_at to the same as created_at
after_create :set_last_activity_at
@@ -61,10 +64,24 @@ class Project < ActiveRecord::Base
update_column(:last_activity_at, self.created_at)
end
+ # update visibility_levet of forks
+ after_update :update_forks_visibility_level
+ def update_forks_visibility_level
+ return unless visibility_level < visibility_level_was
+
+ forks.each do |forked_project|
+ if forked_project.visibility_level > visibility_level
+ forked_project.visibility_level = visibility_level
+ forked_project.save!
+ end
+ end
+ end
+
ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags
attr_accessor :new_default_branch
+ attr_accessor :old_path_with_namespace
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
@@ -75,10 +92,10 @@ class Project < ActiveRecord::Base
# Project services
has_many :services
- has_one :gitlab_ci_service, dependent: :destroy
has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
+ has_one :builds_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
has_one :hipchat_service, dependent: :destroy
@@ -97,9 +114,12 @@ class Project < ActiveRecord::Base
has_one :gitlab_issue_tracker_service, dependent: :destroy
has_one :external_wiki_service, dependent: :destroy
- has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
+ has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
+ has_one :forked_from_project, through: :forked_project_link
+
+ has_many :forked_project_links, foreign_key: "forked_from_project_id"
+ has_many :forks, through: :forked_project_links, source: :forked_to_project
- has_one :forked_from_project, through: :forked_project_link
# Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed
@@ -119,11 +139,21 @@ class Project < ActiveRecord::Base
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user
- has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
- has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :releases, dependent: :destroy
+ has_many :lfs_objects_projects, dependent: :destroy
+ has_many :lfs_objects, through: :lfs_objects_projects
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
- has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id
+
+ 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 :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
+
+ accepts_nested_attributes_for :variables, allow_destroy: true
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -148,7 +178,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
- format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' },
+ url: { protocols: %w(ssh git http https) },
if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
@@ -156,6 +186,11 @@ class Project < ActiveRecord::Base
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
+ before_validation :set_runners_token_token
+ def set_runners_token_token
+ self.runners_token = SecureRandom.hex(15) if self.runners_token.blank?
+ end
+
mount_uploader :avatar, AvatarUploader
# Scopes
@@ -243,11 +278,16 @@ class Project < ActiveRecord::Base
# 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).
- unscoped.
+ projects = unscoped.
joins(:namespace).
- iwhere('namespaces.path' => namespace_path).
- iwhere('projects.path' => project_path).
- take
+ iwhere('namespaces.path' => namespace_path)
+
+ projects.find_by('projects.path' => project_path) ||
+ projects.iwhere('projects.path' => project_path).take
+ end
+
+ def find_by_ci_id(id)
+ find_by(ci_id: id.to_i)
end
def visibility_levels
@@ -280,6 +320,10 @@ 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))
+ end
end
def team
@@ -304,15 +348,17 @@ class Project < ActiveRecord::Base
def add_import_job
if forked?
- unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path)
- import_fail
- end
+ RepositoryForkWorker.perform_async(self.id, forked_from_project.path_with_namespace, self.namespace.path)
else
- RepositoryImportWorker.perform_async(id)
+ RepositoryImportWorker.perform_async(self.id)
end
end
def clear_import_data
+ update(import_error: nil)
+
+ ProjectCacheWorker.perform_async(self.id)
+
self.import_data.destroy if self.import_data
end
@@ -340,6 +386,14 @@ class Project < ActiveRecord::Base
import_status == 'finished'
end
+ def safe_import_url
+ result = URI.parse(self.import_url)
+ result.password = '*****' unless result.password.nil?
+ result.to_s
+ rescue
+ original_url
+ end
+
def check_limit
unless creator.can_create_project? or namespace.kind == 'group'
errors[:limit_reached] << ("Your project limit is #{creator.projects_limit} projects! Please contact your administrator to increase it")
@@ -413,7 +467,7 @@ class Project < ActiveRecord::Base
end
def external_issue_tracker
- @external_issues_tracker ||= external_issues_trackers.select(&:activated?).first
+ @external_issues_tracker ||= external_issues_trackers.find(&:activated?)
end
def can_have_issues_tracker_id?
@@ -454,16 +508,16 @@ class Project < ActiveRecord::Base
list.find { |service| service.to_param == name }
end
- def gitlab_ci?
- gitlab_ci_service && gitlab_ci_service.active && gitlab_ci_project.present?
- end
-
def ci_services
services.select { |service| service.category == :ci }
end
def ci_service
- @ci_service ||= ci_services.select(&:activated?).first
+ @ci_service ||= ci_services.find(&:activated?)
+ end
+
+ def jira_tracker?
+ issues_tracker.to_param == 'jira'
end
def avatar_type
@@ -502,7 +556,9 @@ class Project < ActiveRecord::Base
end
def send_move_instructions(old_path_with_namespace)
- NotificationService.new.project_was_moved(self, old_path_with_namespace)
+ # New project path needs to be committed to the DB or notification will
+ # retrieve stale information
+ run_after_commit { NotificationService.new.project_was_moved(self, old_path_with_namespace) }
end
def owner
@@ -514,7 +570,7 @@ class Project < ActiveRecord::Base
end
def project_member_by_name_or_email(name = nil, email = nil)
- user = users.where('name like ? or email like ?', name, email).first
+ user = users.find_by('name like ? or email like ?', name, email)
project_members.where(user: user) if user
end
@@ -567,7 +623,7 @@ class Project < ActiveRecord::Base
end
def empty_repo?
- !repository.exists? || repository.empty?
+ !repository.exists? || !repository.has_visible_content?
end
def repo
@@ -646,6 +702,12 @@ class Project < ActiveRecord::Base
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
+
+ @old_path_with_namespace = old_path_with_namespace
+
+ SystemHooksService.new.execute_hooks_for(self, :rename)
+
+ @repository = nil
rescue
# Returning false does not rollback after_* transaction but gives
# us information about failing some of tasks
@@ -688,7 +750,7 @@ class Project < ActiveRecord::Base
end
def project_member(user)
- project_members.where(user_id: user).first
+ project_members.find_by(user_id: user)
end
def default_branch
@@ -713,6 +775,8 @@ class Project < ActiveRecord::Base
end
def change_head(branch)
+ # Cached divergent commit counts are based on repository head
+ repository.expire_branch_cache
gitlab_shell.update_repository_head(self.path_with_namespace, branch)
reload_default_branch
end
@@ -730,7 +794,7 @@ class Project < ActiveRecord::Base
end
def forks_count
- ForkedProjectLink.where(forked_from_project_id: self.id).count
+ forks.count
end
def find_label(name)
@@ -765,6 +829,10 @@ class Project < ActiveRecord::Base
false
end
+ def jira_tracker_active?
+ jira_tracker? && jira_service.active
+ end
+
def ci_commit(sha)
ci_commits.find_by(sha: sha)
end
@@ -773,13 +841,56 @@ class Project < ActiveRecord::Base
ci_commit(sha) || ci_commits.create(sha: sha)
end
- def ensure_gitlab_ci_project
- gitlab_ci_project || create_gitlab_ci_project
+ def enable_ci
+ self.builds_enabled = true
end
- def enable_ci
- service = gitlab_ci_service || create_gitlab_ci_service
- service.active = true
- service.save
+ def unlink_fork
+ if forked?
+ forked_from_project.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << self
+ end
+
+ forked_project_link.destroy
+ end
+ end
+
+ def any_runners?(&block)
+ if runners.active.any?(&block)
+ return true
+ end
+
+ shared_runners_enabled? && Ci::Runner.shared.active.any?(&block)
+ end
+
+ def valid_runners_token? token
+ self.runners_token && self.runners_token == token
+ end
+
+ # TODO (ayufan): For now we use runners_token (backward compatibility)
+ # In 8.4 every build will have its own individual token valid for time of build
+ def valid_build_token? token
+ self.builds_enabled? && self.runners_token && self.runners_token == token
+ end
+
+ def build_coverage_enabled?
+ build_coverage_regex.present?
+ end
+
+ def build_timeout_in_minutes
+ build_timeout / 60
+ end
+
+ def build_timeout_in_minutes=(value)
+ self.build_timeout = value.to_i * 60
+ end
+
+ def open_issues_count
+ issues.opened.count
+ end
+
+ def visibility_level_allowed?(level)
+ return true unless forked?
+ Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
end
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index e6e16058d41..7d367e40037 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -40,8 +40,8 @@ get the commit comment added to it.
You can also close a task with a message containing: `fix #123456`.
-You can find your Api Keys here:
-http://developer.asana.com/documentation/#api_keys'
+You can create a Personal Access Token here:
+http://app.asana.com/-/account_api'
end
def to_param
@@ -53,14 +53,12 @@ http://developer.asana.com/documentation/#api_keys'
{
type: 'text',
name: 'api_key',
- placeholder: 'User API token. User must have access to task,
-all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
},
{
type: 'text',
name: 'restrict_to_branch',
- placeholder: 'Comma-separated list of branches which will be
-automatically inspected. Leave blank to include all branches.'
+ placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
]
end
@@ -69,58 +67,58 @@ automatically inspected. Leave blank to include all branches.'
%w(push)
end
+ def client
+ @_client ||= begin
+ Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
def execute(data)
return unless supported_events.include?(data[:object_kind])
- Asana.configure do |client|
- client.api_key = api_key
- end
-
- user = data[:user_name]
+ # check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
-
branch_restriction = restrict_to_branch.to_s
-
- # check the branch restriction is poplulated and branch is not included
if branch_restriction.length > 0 && branch_restriction.index(branch).nil?
return
end
+ user = data[:user_name]
project_name = project.name_with_namespace
- push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
data[:commits].each do |commit|
- check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
+ push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
+ check_commit(commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
- task_list = []
- close_list = []
-
- message.split("\n").each do |line|
- # look for a task ID or a full Asana url
- task_list.concat(line.scan(/#(\d+)/))
- task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/))
- # look for a word starting with 'fix' followed by a task ID
- close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i))
- end
-
- # post commit to every taskid found
- task_list.each do |taskid|
- task = Asana::Task.find(taskid[0])
-
- if task
- task.create_story(text: push_msg + ' ' + message)
- end
- end
-
- # close all tasks that had 'fix(ed/es/ing) #:id' in them
- close_list.each do |taskid|
- task = Asana::Task.find(taskid.last)
-
- if task
- task.modify(completed: true)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/0/1234
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = Asana::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue => e
+ Rails.logger.error(e.message)
+ next
end
end
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index d31b12f539e..aa8746beb80 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -23,19 +23,14 @@ class BambooService < CiService
prop_accessor :bamboo_url, :build_key, :username, :password
- validates :bamboo_url,
- presence: true,
- format: { with: /\A#{URI.regexp}\z/ },
- if: :activated?
+ validates :bamboo_url, presence: true, url: true, if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
presence: true,
- if: ->(service) { service.password? },
- if: :activated?
+ if: ->(service) { service.activated? && service.password }
validates :password,
presence: true,
- if: ->(service) { service.username? },
- if: :activated?
+ if: ->(service) { service.activated? && service.username }
attr_accessor :response
@@ -84,7 +79,7 @@ class BambooService < CiService
def supported_events
%w(push)
end
-
+
def build_info(sha)
url = URI.parse("#{bamboo_url}/rest/api/latest/result?label=#{sha}")
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 40058b53df5..199ee3a9d0d 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -37,7 +37,7 @@ class BuildkiteService < CiService
def compose_service_hook
hook = service_hook || build_service_hook
hook.url = webhook_url
- hook.enable_ssl_verification = enable_ssl_verification
+ hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
new file mode 100644
index 00000000000..8247c79fc33
--- /dev/null
+++ b/app/models/project_services/builds_email_service.rb
@@ -0,0 +1,90 @@
+# == 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
+#
+
+class BuildsEmailService < Service
+ prop_accessor :recipients
+ boolean_accessor :add_pusher
+ boolean_accessor :notify_only_broken_builds
+ validates :recipients, presence: true, if: :activated?
+
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_builds = true
+ end
+ end
+
+ def title
+ 'Builds emails'
+ end
+
+ def description
+ 'Email the builds status to a list of recipients.'
+ end
+
+ def to_param
+ 'builds_email'
+ end
+
+ def supported_events
+ %w(build)
+ end
+
+ def execute(push_data)
+ return unless supported_events.include?(push_data[:object_kind])
+
+ if should_build_be_notified?(push_data)
+ BuildEmailWorker.perform_async(
+ push_data[:build_id],
+ all_recipients(push_data),
+ push_data,
+ )
+ end
+ end
+
+ def fields
+ [
+ { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
+ { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
+ ]
+ end
+
+ def should_build_be_notified?(data)
+ case data[:build_status]
+ when 'success'
+ !notify_only_broken_builds?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
+
+ def all_recipients(data)
+ all_recipients = recipients.split(',')
+
+ if add_pusher? && data[:user][:email]
+ all_recipients << "#{data[:user][:email]}"
+ end
+
+ all_recipients
+ end
+end
diff --git a/app/models/project_services/ci/hip_chat_message.rb b/app/models/project_services/ci/hip_chat_message.rb
deleted file mode 100644
index cbf325cc525..00000000000
--- a/app/models/project_services/ci/hip_chat_message.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-module Ci
- class HipChatMessage
- include Gitlab::Application.routes.url_helpers
-
- attr_reader :build
-
- def initialize(build)
- @build = build
- end
-
- def to_s
- lines = Array.new
- lines.push("<a href=\"#{ci_project_url(project)}\">#{project.name}</a> - ")
- lines.push("<a href=\"#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}\">Commit ##{commit.id}</a></br>")
- lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>")
- lines.push("#{humanized_status(commit_status)} in #{commit.duration} second(s).")
- lines.join('')
- end
-
- def status_color(build_or_commit=nil)
- build_or_commit ||= commit_status
- case build_or_commit
- when :success
- 'green'
- when :failed, :canceled
- 'red'
- else # :pending, :running or unknown
- 'yellow'
- end
- end
-
- def notify?
- [:failed, :canceled].include?(commit_status)
- end
-
-
- private
-
- def commit
- build.commit
- end
-
- def project
- commit.project
- end
-
- def build_status
- build.status.to_sym
- end
-
- def commit_status
- commit.status.to_sym
- end
-
- def humanized_status(build_or_commit=nil)
- build_or_commit ||= commit_status
- case build_or_commit
- when :pending
- "Pending"
- when :running
- "Running"
- when :failed
- "Failed"
- when :success
- "Successful"
- when :canceled
- "Canceled"
- else
- "Unknown"
- end
- end
- end
-end
diff --git a/app/models/project_services/ci/hip_chat_service.rb b/app/models/project_services/ci/hip_chat_service.rb
deleted file mode 100644
index f17993d9f3b..00000000000
--- a/app/models/project_services/ci/hip_chat_service.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-module Ci
- class HipChatService < Ci::Service
- prop_accessor :hipchat_token, :hipchat_room, :hipchat_server
- boolean_accessor :notify_only_broken_builds
- validates :hipchat_token, presence: true, if: :activated?
- validates :hipchat_room, presence: true, if: :activated?
- default_value_for :notify_only_broken_builds, true
-
- def title
- "HipChat"
- end
-
- def description
- "Private group chat, video chat, instant messaging for teams"
- end
-
- def help
- end
-
- def to_param
- 'hip_chat'
- end
-
- def fields
- [
- { type: 'text', name: 'hipchat_token', label: 'Token', placeholder: '' },
- { type: 'text', name: 'hipchat_room', label: 'Room', placeholder: '' },
- { type: 'text', name: 'hipchat_server', label: 'Server', placeholder: 'https://hipchat.example.com', help: 'Leave blank for default' },
- { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
- ]
- end
-
- def can_execute?(build)
- return if build.allow_failure?
-
- commit = build.commit
- return unless commit
- return unless commit.latest_builds.include? build
-
- case commit.status.to_sym
- when :failed
- true
- when :success
- true unless notify_only_broken_builds?
- else
- false
- end
- end
-
- def execute(build)
- msg = Ci::HipChatMessage.new(build)
- opts = default_options.merge(
- token: hipchat_token,
- room: hipchat_room,
- server: server_url,
- color: msg.status_color,
- notify: msg.notify?
- )
- Ci::HipChatNotifierWorker.perform_async(msg.to_s, opts)
- end
-
- private
-
- def default_options
- {
- service_name: 'GitLab CI',
- message_format: 'html'
- }
- end
-
- def server_url
- if hipchat_server.blank?
- 'https://api.hipchat.com'
- else
- hipchat_server
- end
- end
- end
-end
diff --git a/app/models/project_services/ci/mail_service.rb b/app/models/project_services/ci/mail_service.rb
deleted file mode 100644
index fd193301001..00000000000
--- a/app/models/project_services/ci/mail_service.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-module Ci
- class MailService < Ci::Service
- delegate :email_recipients, :email_recipients=,
- :email_add_pusher, :email_add_pusher=,
- :email_only_broken_builds, :email_only_broken_builds=, to: :project, prefix: false
-
- before_save :update_project
-
- default_value_for :active, true
-
- def title
- 'Mail'
- end
-
- def description
- 'Email notification'
- end
-
- def to_param
- 'mail'
- end
-
- def fields
- [
- { type: 'text', name: 'email_recipients', label: 'Recipients', help: 'Whitespace-separated list of recipient addresses' },
- { type: 'checkbox', name: 'email_add_pusher', label: 'Add pusher to recipients list' },
- { type: 'checkbox', name: 'email_only_broken_builds', label: 'Notify only broken builds' }
- ]
- end
-
- def can_execute?(build)
- return if build.allow_failure?
-
- # it doesn't make sense to send emails for retried builds
- commit = build.commit
- return unless commit
- return unless commit.latest_builds.include?(build)
-
- case build.status.to_sym
- when :failed
- true
- when :success
- true unless email_only_broken_builds
- else
- false
- end
- end
-
- def execute(build)
- build.project_recipients.each do |recipient|
- case build.status.to_sym
- when :success
- mailer.build_success_email(build.id, recipient)
- when :failed
- mailer.build_fail_email(build.id, recipient)
- end
- end
- end
-
- private
-
- def update_project
- project.save!
- end
-
- def mailer
- Ci::Notify.delay
- end
- end
-end
diff --git a/app/models/project_services/ci/slack_message.rb b/app/models/project_services/ci/slack_message.rb
deleted file mode 100644
index dc050a3fc59..00000000000
--- a/app/models/project_services/ci/slack_message.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'slack-notifier'
-
-module Ci
- class SlackMessage
- include Gitlab::Application.routes.url_helpers
-
- def initialize(commit)
- @commit = commit
- end
-
- def pretext
- ''
- end
-
- def color
- attachment_color
- end
-
- def fallback
- format(attachment_message)
- end
-
- def attachments
- fields = []
-
- commit.latest_builds.each do |build|
- next if build.allow_failure?
- next unless build.failed?
- fields << {
- title: build.name,
- value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
- }
- end
-
- [{
- text: attachment_message,
- color: attachment_color,
- fields: fields
- }]
- end
-
- private
-
- attr_reader :commit
-
- def attachment_message
- out = "<#{ci_project_url(project)}|#{project_name}>: "
- out << "Commit <#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> "
- out << "(<#{commit_sha_link}|#{commit.short_sha}>) "
- out << "of <#{commit_ref_link}|#{commit.ref}> "
- out << "by #{commit.git_author_name} " if commit.git_author_name
- out << "#{commit_status} in "
- out << "#{commit.duration} second(s)"
- end
-
- def format(string)
- Slack::Notifier::LinkFormatter.format(string)
- end
-
- def project
- commit.project
- end
-
- def project_name
- project.name
- end
-
- def commit_sha_link
- "#{project.gitlab_url}/commit/#{commit.sha}"
- end
-
- def commit_ref_link
- "#{project.gitlab_url}/commits/#{commit.ref}"
- end
-
- def attachment_color
- if commit.success?
- 'good'
- else
- 'danger'
- end
- end
-
- def commit_status
- if commit.success?
- 'succeeded'
- else
- 'failed'
- end
- end
- end
-end
diff --git a/app/models/project_services/ci/slack_service.rb b/app/models/project_services/ci/slack_service.rb
deleted file mode 100644
index ee8e4988826..00000000000
--- a/app/models/project_services/ci/slack_service.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-module Ci
- class SlackService < Ci::Service
- prop_accessor :webhook
- boolean_accessor :notify_only_broken_builds
- validates :webhook, presence: true, if: :activated?
-
- default_value_for :notify_only_broken_builds, true
-
- def title
- 'Slack'
- end
-
- def description
- 'A team communication tool for the 21st century'
- end
-
- def to_param
- 'slack'
- end
-
- def help
- 'Visit https://www.slack.com/services/new/incoming-webhook. Then copy link and save project!' unless webhook.present?
- end
-
- def fields
- [
- { type: 'text', name: 'webhook', label: 'Webhook URL', placeholder: '' },
- { type: 'checkbox', name: 'notify_only_broken_builds', label: 'Notify only broken builds' }
- ]
- end
-
- def can_execute?(build)
- return if build.allow_failure?
-
- commit = build.commit
- return unless commit
- return unless commit.latest_builds.include?(build)
-
- case commit.status.to_sym
- when :failed
- true
- when :success
- true unless notify_only_broken_builds?
- else
- false
- end
- end
-
- def execute(build)
- message = Ci::SlackMessage.new(build.commit)
- options = default_options.merge(
- color: message.color,
- fallback: message.fallback,
- attachments: message.attachments
- )
- Ci::SlackNotifierWorker.perform_async(webhook, message.pretext, options)
- end
-
- private
-
- def default_options
- {
- username: 'GitLab CI'
- }
- end
- end
-end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index c73c4b058a1..08e5ccb3855 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -19,21 +19,19 @@
#
class DroneCiService < CiService
-
+
prop_accessor :drone_url, :token, :enable_ssl_verification
- validates :drone_url,
- presence: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated?
- validates :token,
- presence: true,
- if: :activated?
+
+ validates :drone_url, presence: true, url: true, if: :activated?
+ validates :token, presence: true, if: :activated?
after_save :compose_service_hook, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
- hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join
- hook.enable_ssl_verification = enable_ssl_verification
+ # If using a service template, project may not be available
+ hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join if project
+ hook.enable_ssl_verification = !!enable_ssl_verification
hook.save
end
@@ -57,16 +55,16 @@ class DroneCiService < CiService
end
def merge_request_status_path(iid, sha = nil, ref = nil)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
"?access_token=#{token}"]
URI.join(*url).to_s
end
def commit_status_path(sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
URI.join(*url).to_s
@@ -113,15 +111,15 @@ class DroneCiService < CiService
end
def merge_request_page(iid, sha, ref)
- url = [drone_url,
+ url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
URI.join(*url).to_s
end
def commit_page(sha, ref)
- url = [drone_url,
- "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"]
URI.join(*url).to_s
@@ -162,10 +160,10 @@ class DroneCiService < CiService
end
def push_valid?(data)
- opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
+ opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
source_branch: Gitlab::Git.ref_name(data[:ref]))
- opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
+ opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
!Gitlab::Git.blank_ref?(data[:after])
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 9c46af7e721..74c57949b4d 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -22,10 +22,8 @@ class ExternalWikiService < Service
include HTTParty
prop_accessor :external_wiki_url
- validates :external_wiki_url,
- presence: true,
- format: { with: /\A#{URI.regexp}\z/ },
- if: :activated?
+
+ validates :external_wiki_url, presence: true, url: true, if: :activated?
def title
'External Wiki'
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 27fc19379f1..15c7c907f7e 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -58,6 +58,6 @@ class FlowdockService < Service
repo_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/compare/%s...%s",
- )
+ )
end
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 91ef267ad79..202fee042e3 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -57,6 +57,6 @@ class GemnasiumService < Service
token: token,
api_key: api_key,
repo: project.repository.path_to_repo
- )
+ )
end
end
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index 4dcd16ede3a..b64d97ce75d 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -18,76 +18,11 @@
# note_events :boolean default(TRUE), not null
#
+# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
class GitlabCiService < CiService
- include Gitlab::Application.routes.url_helpers
-
- after_save :compose_service_hook, if: :activated?
- after_save :ensure_gitlab_ci_project, if: :activated?
-
- def compose_service_hook
- hook = service_hook || build_service_hook
- hook.save
- end
-
- def ensure_gitlab_ci_project
- project.ensure_gitlab_ci_project
- end
-
- def supported_events
- %w(push tag_push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- ci_project = project.gitlab_ci_project
- if ci_project
- current_user = User.find_by(id: data[:user_id])
- Ci::CreateCommitService.new.execute(ci_project, current_user, data)
- end
- end
-
- def token
- if project.gitlab_ci_project.present?
- project.gitlab_ci_project.token
- end
- end
-
- def get_ci_commit(sha, ref)
- Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha!(sha)
- end
-
- def commit_status(sha, ref)
- get_ci_commit(sha, ref).status
- rescue ActiveRecord::RecordNotFound
- :error
- end
-
- def commit_coverage(sha, ref)
- get_ci_commit(sha, ref).coverage
- rescue ActiveRecord::RecordNotFound
- :error
- end
-
- def build_page(sha, ref)
- if project.gitlab_ci_project.present?
- ci_namespace_project_commit_url(project.namespace, project, sha)
- end
- end
-
- def title
- 'GitLab CI'
- end
-
- def description
- 'Continuous integration server from GitLab'
- end
-
- def to_param
- 'gitlab_ci'
- end
-
- def fields
- []
+ # We override the active accessor to always make GitLabCiService disabled
+ # Otherwise the GitLabCiService can be picked, but should never be since it's deprecated
+ def active
+ false
end
end
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index af2840a57f0..1e1686a11c6 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -22,8 +22,16 @@ class HipchatService < Service
MAX_COMMITS = 3
prop_accessor :token, :room, :server, :notify, :color, :api_version
+ boolean_accessor :notify_only_broken_builds
validates :token, presence: true, if: :activated?
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_builds = true
+ end
+ end
+
def title
'HipChat'
end
@@ -45,12 +53,13 @@ class HipchatService < Service
{ type: 'text', name: 'api_version',
placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
- placeholder: 'Leave blank for default. https://hipchat.example.com' }
+ placeholder: 'Leave blank for default. https://hipchat.example.com' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
]
end
def supported_events
- %w(push issue merge_request note tag_push)
+ %w(push issue merge_request note tag_push build)
end
def execute(data)
@@ -94,6 +103,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data)
when "note"
create_note_message(data)
+ when "build"
+ create_build_message(data) if should_build_be_notified?(data)
end
end
@@ -235,6 +246,20 @@ class HipchatService < Service
message
end
+ def create_build_message(data)
+ ref_type = data[:tag] ? 'tag' : 'branch'
+ ref = data[:ref]
+ sha = data[:sha]
+ user_name = data[:commit][:author_name]
+ status = data[:commit][:status]
+ duration = data[:commit][:duration]
+
+ branch_link = "<a href=\"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"
+ commit_link = "<a href=\"#{project_url}/commit/#{URI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+
+ "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+ end
+
def project_name
project.name_with_namespace.gsub(/\s/, '')
end
@@ -250,4 +275,24 @@ class HipchatService < Service
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
+
+ def humanized_status(status)
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def should_build_be_notified?(data)
+ case data[:commit][:status]
+ when 'success'
+ !notify_only_broken_builds?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 35e30b1cb0b..e216f406e1c 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -19,9 +19,24 @@
#
class JiraService < IssueTrackerService
+ include HTTParty
include Gitlab::Application.routes.url_helpers
- prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+ DEFAULT_API_VERSION = 2
+
+ prop_accessor :username, :password, :api_url, :jira_issue_transition_id,
+ :title, :description, :project_url, :issues_url, :new_issue_url
+
+ before_validation :set_api_url, :set_jira_issue_transition_id
+
+ before_update :reset_password
+
+ def reset_password
+ # don't reset the password if a new one is provided
+ if api_url_changed? && !password_touched?
+ self.password = nil
+ end
+ end
def help
line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
@@ -54,4 +69,228 @@ class JiraService < IssueTrackerService
def to_param
'jira'
end
+
+ def fields
+ super.push(
+ { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' },
+ { type: 'text', name: 'username', placeholder: '' },
+ { type: 'password', name: 'password', placeholder: '' },
+ { type: 'text', name: 'jira_issue_transition_id', placeholder: '2' }
+ )
+ end
+
+ def execute(push, issue = nil)
+ if issue.nil?
+ # No specific issue, that means
+ # we just want to test settings
+ test_settings
+ else
+ close_issue(push, issue)
+ end
+ end
+
+ def create_cross_reference_note(mentioned, noteable, author)
+ issue_name = mentioned.id
+ project = self.project
+ noteable_name = noteable.class.name.underscore.downcase
+ noteable_id = if noteable.is_a?(Commit)
+ noteable.id
+ else
+ noteable.iid
+ end
+
+ entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
+
+ data = {
+ user: {
+ name: author.name,
+ url: resource_url(user_path(author)),
+ },
+ project: {
+ name: project.path_with_namespace,
+ url: resource_url(namespace_project_path(project.namespace, project))
+ },
+ entity: {
+ name: noteable_name.humanize.downcase,
+ url: entity_url
+ }
+ }
+
+ add_comment(data, issue_name)
+ end
+
+ def test_settings
+ result = JiraService.get(
+ jira_api_test_url,
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Basic #{auth}"
+ }
+ )
+
+ case result.code
+ when 201, 200
+ Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.")
+ true
+ else
+ Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}")
+ false
+ end
+ rescue Errno::ECONNREFUSED => e
+ Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}."
+ false
+ end
+
+ private
+
+ def build_api_url_from_project_url
+ server = URI(project_url)
+ 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}"
+ rescue
+ "" # looks like project URL was not valid
+ end
+
+ def set_api_url
+ self.api_url = build_api_url_from_project_url if self.api_url.blank?
+ end
+
+ def set_jira_issue_transition_id
+ self.jira_issue_transition_id ||= "2"
+ end
+
+ def close_issue(entity, issue)
+ commit_id = if entity.is_a?(Commit)
+ entity.id
+ elsif entity.is_a?(MergeRequest)
+ entity.last_commit.id
+ end
+ commit_url = build_entity_url(:commit, commit_id)
+
+ # Depending on the JIRA project's workflow, a comment during transition
+ # may or may not be allowed. Split the operation in to two calls so the
+ # comment always works.
+ transition_issue(issue)
+ add_issue_solved_comment(issue, commit_id, commit_url)
+ end
+
+ def transition_issue(issue)
+ message = {
+ transition: {
+ id: jira_issue_transition_id
+ }
+ }
+ send_message(close_issue_url(issue.iid), message.to_json)
+ end
+
+ def add_issue_solved_comment(issue, commit_id, commit_url)
+ comment = {
+ body: "Issue solved with [#{commit_id}|#{commit_url}]."
+ }
+
+ send_message(comment_url(issue.iid), comment.to_json)
+ end
+
+ def add_comment(data, issue_name)
+ url = comment_url(issue_name)
+ user_name = data[:user][:name]
+ user_url = data[:user][:url]
+ entity_name = data[:entity][:name]
+ entity_url = data[:entity][:url]
+ project_name = data[:project][:name]
+
+ message = {
+ body: "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]."
+ }
+
+ unless existing_comment?(issue_name, message[:body])
+ send_message(url, message.to_json)
+ end
+ end
+
+
+ def auth
+ require 'base64'
+ Base64.urlsafe_encode64("#{self.username}:#{self.password}")
+ end
+
+ def send_message(url, message)
+ result = JiraService.post(
+ url,
+ body: message,
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Basic #{auth}"
+ }
+ )
+
+ message = case result.code
+ when 201, 200, 204
+ "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}."
+ when 401
+ "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again."
+ else
+ "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}"
+ end
+
+ Rails.logger.info(message)
+ message
+ rescue URI::InvalidURIError, Errno::ECONNREFUSED => e
+ Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}."
+ end
+
+ def existing_comment?(issue_name, new_comment)
+ result = JiraService.get(
+ comment_url(issue_name),
+ headers: {
+ 'Content-Type' => 'application/json',
+ 'Authorization' => "Basic #{auth}"
+ }
+ )
+
+ case result.code
+ when 201, 200
+ existing_comments = JSON.parse(result.body)['comments']
+
+ if existing_comments.present?
+ return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any?
+ end
+ end
+
+ false
+ rescue JSON::ParserError
+ false
+ end
+
+ def resource_url(resource)
+ "#{Settings.gitlab['url'].chomp("/")}#{resource}"
+ end
+
+ def build_entity_url(entity_name, entity_id)
+ resource_url(
+ polymorphic_url(
+ [
+ self.project.namespace.becomes(Namespace),
+ self.project,
+ entity_name
+ ],
+ id: entity_id,
+ routing_type: :path
+ )
+ )
+ end
+
+ def close_issue_url(issue_name)
+ "#{self.api_url}/issue/#{issue_name}/transitions"
+ end
+
+ def comment_url(issue_name)
+ "#{self.api_url}/issue/#{issue_name}/comment"
+ end
+
+ def jira_api_test_url
+ "#{self.api_url}/myself"
+ end
end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 7cd5e892507..375b4534d07 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -20,8 +20,16 @@
class SlackService < Service
prop_accessor :webhook, :username, :channel
+ boolean_accessor :notify_only_broken_builds
validates :webhook, presence: true, if: :activated?
+ def initialize_properties
+ if properties.nil?
+ self.properties = {}
+ self.notify_only_broken_builds = true
+ end
+ end
+
def title
'Slack'
end
@@ -45,12 +53,13 @@ class SlackService < Service
{ type: 'text', name: 'webhook',
placeholder: 'https://hooks.slack.com/services/...' },
{ type: 'text', name: 'username', placeholder: 'username' },
- { type: 'text', name: 'channel', placeholder: '#channel' }
+ { type: 'text', name: 'channel', placeholder: '#channel' },
+ { type: 'checkbox', name: 'notify_only_broken_builds' },
]
end
def supported_events
- %w(push issue merge_request note tag_push)
+ %w(push issue merge_request note tag_push build)
end
def execute(data)
@@ -78,6 +87,8 @@ class SlackService < Service
MergeMessage.new(data) unless is_update?(data)
when "note"
NoteMessage.new(data)
+ when "build"
+ BuildMessage.new(data) if should_build_be_notified?(data)
end
opt = {}
@@ -86,7 +97,7 @@ class SlackService < Service
if message
notifier = Slack::Notifier.new(webhook, opt)
- notifier.ping(message.pretext, attachments: message.attachments)
+ notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
end
end
@@ -103,9 +114,21 @@ class SlackService < Service
def is_update?(data)
data[:object_attributes][:action] == 'update'
end
+
+ def should_build_be_notified?(data)
+ case data[:commit][:status]
+ when 'success'
+ !notify_only_broken_builds?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
end
require "slack_service/issue_message"
require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
+require "slack_service/build_message"
diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/slack_service/base_message.rb
index aa00d6061a1..f1182824687 100644
--- a/app/models/project_services/slack_service/base_message.rb
+++ b/app/models/project_services/slack_service/base_message.rb
@@ -10,6 +10,9 @@ class SlackService
format(message)
end
+ def fallback
+ end
+
def attachments
raise NotImplementedError
end
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb
new file mode 100644
index 00000000000..c124cad4afd
--- /dev/null
+++ b/app/models/project_services/slack_service/build_message.rb
@@ -0,0 +1,82 @@
+class SlackService
+ class BuildMessage < BaseMessage
+ attr_reader :sha
+ attr_reader :ref_type
+ attr_reader :ref
+ attr_reader :status
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :user_name
+ attr_reader :duration
+
+ def initialize(params, commit = true)
+ @sha = params[:sha]
+ @ref_type = params[:tag] ? 'tag' : 'branch'
+ @ref = params[:ref]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+ @status = params[:commit][:status]
+ @user_name = params[:commit][:author_name]
+ @duration = params[:commit][:duration]
+ end
+
+ def pretext
+ ''
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ [{ text: format(message), color: attachment_color }]
+ end
+
+ private
+
+ def message
+ "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} second(s)"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ if status == 'success'
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def branch_url
+ "#{project_url}/commits/#{ref}"
+ end
+
+ def branch_link
+ "[#{ref}](#{branch_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def commit_url
+ "#{project_url}/commit/#{sha}/builds"
+ end
+
+ def commit_link
+ "[#{Commit.truncate_sha(sha)}](#{commit_url})"
+ end
+ end
+end
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 074478b292d..b15d9a14677 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -45,30 +45,27 @@ class SlackService
def create_commit_note(commit)
commit_sha = commit[:id]
commit_sha = Commit.truncate_sha(commit_sha)
- commit_link = "[commit #{commit_sha}](#{@note_url})"
- title = format_title(commit[:message])
- @message = "#{@user_name} commented on #{commit_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[commit #{commit_sha}](#{@note_url})",
+ format_title(commit[:message]))
end
def create_issue_note(issue)
- issue_iid = issue[:iid]
- note_link = "[issue ##{issue_iid}](#{@note_url})"
- title = format_title(issue[:title])
- @message = "#{@user_name} commented on #{note_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[issue ##{issue[:iid]}](#{@note_url})",
+ format_title(issue[:title]))
end
def create_merge_note(merge_request)
- merge_request_id = merge_request[:iid]
- merge_request_link = "[merge request ##{merge_request_id}](#{@note_url})"
- title = format_title(merge_request[:title])
- @message = "#{@user_name} commented on #{merge_request_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[merge request ##{merge_request[:iid]}](#{@note_url})",
+ format_title(merge_request[:title]))
end
def create_snippet_note(snippet)
- snippet_id = snippet[:id]
- snippet_link = "[snippet ##{snippet_id}](#{@note_url})"
- title = format_title(snippet[:title])
- @message = "#{@user_name} commented on #{snippet_link} in #{project_link}: *#{title}*"
+ commented_on_message(
+ "[snippet ##{snippet[:id]}](#{@note_url})",
+ format_title(snippet[:title]))
end
def description_message
@@ -78,5 +75,9 @@ class SlackService
def project_link
"[#{@project_name}](#{@project_url})"
end
+
+ def commented_on_message(target_link, title)
+ @message = "#{@user_name} commented on #{target_link} in #{project_link}: *#{title}*"
+ end
end
end
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 0b022461250..a63700693d7 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -23,16 +23,14 @@ class TeamcityService < CiService
prop_accessor :teamcity_url, :build_type, :username, :password
- validates :teamcity_url,
- presence: true,
- format: { with: /\A#{URI.regexp}\z/ }, if: :activated?
+ validates :teamcity_url, presence: true, url: true, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
- if: ->(service) { service.password? }, if: :activated?
+ if: ->(service) { service.activated? && service.password }
validates :password,
presence: true,
- if: ->(service) { service.username? }, if: :activated?
+ if: ->(service) { service.activated? && service.username }
attr_accessor :response
@@ -147,6 +145,6 @@ class TeamcityService < CiService
'</build>',
headers: { 'Content-type' => 'application/xml' },
basic_auth: auth
- )
+ )
end
end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 231973fa543..b5fec38378b 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -86,6 +86,8 @@ class ProjectWiki
commit = commit_details(:created, message, title)
wiki.write_page(title, format, content, commit)
+
+ update_project_activity
rescue Gollum::DuplicatePageError => e
@error_message = "Duplicate page: #{e.message}"
return false
@@ -95,10 +97,14 @@ class ProjectWiki
commit = commit_details(:updated, message, page.title)
wiki.update_page(page, page.name, format, content, commit)
+
+ update_project_activity
end
def delete_page(page, message = nil)
wiki.delete_page(page, commit_details(:deleted, message, page.title))
+
+ update_project_activity
end
def page_title_and_dir(title)
@@ -146,4 +152,8 @@ class ProjectWiki
def path_to_repo
@path_to_repo ||= File.join(Gitlab.config.gitlab_shell.repos_path, "#{path_with_namespace}.git")
end
+
+ def update_project_activity
+ @project.touch(:last_activity_at)
+ end
end
diff --git a/app/models/release.rb b/app/models/release.rb
new file mode 100644
index 00000000000..89f70278af5
--- /dev/null
+++ b/app/models/release.rb
@@ -0,0 +1,17 @@
+# == Schema Information
+#
+# Table name: releases
+#
+# id :integer not null, primary key
+# tag :string(255)
+# description :text
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+class Release < ActiveRecord::Base
+ belongs_to :project
+
+ validates :description, :project, :tag, presence: true
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 0808896fd87..6ecd2d2f27e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,12 +1,11 @@
require 'securerandom'
class Repository
- class PreReceiveError < StandardError; end
class CommitError < StandardError; end
include Gitlab::ShellAdapter
- attr_accessor :raw_repository, :path_with_namespace, :project
+ attr_accessor :path_with_namespace, :project
def self.clean_old_archives
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
@@ -19,14 +18,18 @@ class Repository
def initialize(path_with_namespace, default_branch = nil, project = nil)
@path_with_namespace = path_with_namespace
@project = project
+ end
- if path_with_namespace
- @raw_repository = Gitlab::Git::Repository.new(path_to_repo)
- @raw_repository.autocrlf = :input
- end
+ def raw_repository
+ return nil unless path_with_namespace
- rescue Gitlab::Git::Repository::NoRepository
- nil
+ @raw_repository ||= begin
+ repo = Gitlab::Git::Repository.new(path_to_repo)
+ repo.autocrlf = :input
+ repo
+ rescue Gitlab::Git::Repository::NoRepository
+ nil
+ end
end
# Return absolute path to repository
@@ -44,6 +47,19 @@ class Repository
raw_repository.empty?
end
+ #
+ # Git repository can contains some hidden refs like:
+ # /refs/notes/*
+ # /refs/git-as-svn/*
+ # /refs/pulls/*
+ # This refs by default not visible in project page and not cloned to client side.
+ #
+ # This method return true if repository contains some content visible in project page.
+ #
+ def has_visible_content?
+ !raw_repository.branches.empty?
+ end
+
def commit(id = 'HEAD')
return nil unless raw_repository
commit = Gitlab::Git::Commit.find(raw_repository, id)
@@ -54,13 +70,18 @@ class Repository
end
def commits(ref, path = nil, limit = nil, offset = nil, skip_merges = false)
- commits = Gitlab::Git::Commit.where(
+ options = {
repo: raw_repository,
ref: ref,
path: path,
limit: limit,
offset: offset,
- )
+ # --follow doesn't play well with --skip. See:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/3574#note_3040520
+ follow: false
+ }
+
+ commits = Gitlab::Git::Commit.where(options)
commits = Commit.decorate(commits, @project) if commits.present?
commits
end
@@ -71,38 +92,62 @@ class Repository
commits
end
+ def find_commits_by_message(query)
+ # Limited to 1000 commits for now, could be parameterized?
+ args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query})
+
+ git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
+ commits = git_log_results.map { |c| commit(c) }
+ commits
+ end
+
def find_branch(name)
- branches.find { |branch| branch.name == name }
+ raw_repository.branches.find { |branch| branch.name == name }
end
def find_tag(name)
- tags.find { |tag| tag.name == name }
+ raw_repository.tags.find { |tag| tag.name == name }
end
- def add_branch(branch_name, ref)
- cache.expire(:branch_names)
- @branches = nil
+ def add_branch(user, branch_name, target)
+ oldrev = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+ target = commit(target).try(:id)
+
+ return false unless target
- gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
+ GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
+ rugged.branches.create(branch_name, target)
+ end
+
+ expire_branches_cache
+ find_branch(branch_name)
end
def add_tag(tag_name, ref, message = nil)
- cache.expire(:tag_names)
- @tags = nil
+ expire_tags_cache
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
- def rm_branch(branch_name)
- cache.expire(:branch_names)
- @branches = nil
+ def rm_branch(user, branch_name)
+ expire_branches_cache
+
+ branch = find_branch(branch_name)
+ oldrev = branch.try(:target)
+ newrev = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
+
+ GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
+ rugged.branches.delete(branch_name)
+ end
- gitlab_shell.rm_branch(path_with_namespace, branch_name)
+ expire_branches_cache
+ true
end
def rm_tag(tag_name)
- cache.expire(:tag_names)
- @tags = nil
+ expire_tags_cache
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -130,11 +175,29 @@ class Repository
def size
cache.fetch(:size) { raw_repository.size }
end
+
+ def diverging_commit_counts(branch)
+ root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
+ cache.fetch(:"diverging_commit_counts_#{branch.name}") do
+ # Rugged seems to throw a `ReferenceError` when given branch_names rather
+ # than SHA-1 hashes
+ number_commits_behind = commits_between(branch.target, root_ref_hash).size
+ number_commits_ahead = commits_between(root_ref_hash, branch.target).size
+
+ { behind: number_commits_behind, ahead: number_commits_ahead }
+ end
+ end
def cache_keys
%i(size branch_names tag_names commit_count
readme version contribution_guide changelog license)
end
+
+ def branch_cache_keys
+ branches.map do |branch|
+ :"diverging_commit_counts_#{branch.name}"
+ end
+ end
def build_cache
cache_keys.each do |key|
@@ -142,12 +205,36 @@ class Repository
send(key)
end
end
+
+ branches.each do |branch|
+ unless cache.exist?(:"diverging_commit_counts_#{branch.name}")
+ send(:diverging_commit_counts, branch)
+ end
+ end
+ end
+
+ def expire_tags_cache
+ cache.expire(:tag_names)
+ @tags = nil
+ end
+
+ def expire_branches_cache
+ cache.expire(:branch_names)
+ @branches = nil
end
def expire_cache
cache_keys.each do |key|
cache.expire(key)
end
+
+ expire_branch_cache
+ end
+
+ def expire_branch_cache
+ branches.each do |branch|
+ cache.expire(:"diverging_commit_counts_#{branch.name}")
+ end
end
def rebuild_cache
@@ -155,6 +242,11 @@ class Repository
cache.expire(key)
send(key)
end
+
+ branches.each do |branch|
+ cache.expire(:"diverging_commit_counts_#{branch.name}")
+ diverging_commit_counts(branch)
+ end
end
def lookup_cache
@@ -218,9 +310,25 @@ class Repository
def license
cache.fetch(:license) do
- tree(:head).blobs.find do |file|
- file.name =~ /\Alicense/i
+ licenses = tree(:head).blobs.find_all do |file|
+ file.name =~ /\A(copying|license|licence)/i
+ end
+
+ preferences = [
+ /\Alicen[sc]e\z/i, # LICENSE, LICENCE
+ /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt
+ /\Acopying\z/i, # COPYING
+ /\Acopying\.(?!lesser)/i, # COPYING.txt
+ /Acopying.lesser/i # COPYING.LESSER
+ ]
+
+ license = nil
+ preferences.each do |r|
+ license = licenses.find { |l| l.name =~ r }
+ break if license
end
+
+ license
end
end
@@ -271,11 +379,22 @@ class Repository
end
def last_commit_for_path(sha, path)
- args = %W(git rev-list --max-count=1 #{sha} -- #{path})
+ args = %W(#{Gitlab.config.git.bin_path} rev-list --max-count=1 #{sha} -- #{path})
sha = Gitlab::Popen.popen(args, path_to_repo).first.strip
commit(sha)
end
+ def next_patch_branch
+ patch_branch_ids = self.branch_names.map do |n|
+ result = n.match(/\Apatch-([0-9]+)\z/)
+ result[1].to_i if result
+ end.compact
+
+ highest_patch_branch_id = patch_branch_ids.max || 0
+
+ "patch-#{highest_patch_branch_id + 1}"
+ end
+
# Remove archives older than 2 hours
def branches_sorted_by(value)
case value
@@ -321,8 +440,8 @@ class Repository
end
end
- def branch_names_contains(sha)
- args = %W(git branch --contains #{sha})
+ 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
if names.respond_to?(:split)
@@ -338,21 +457,12 @@ class Repository
end
end
- def tag_names_contains(sha)
- args = %W(git tag --contains #{sha})
- names = Gitlab::Popen.popen(args, path_to_repo).first
-
- if names.respond_to?(:split)
- names = names.split("\n").map(&:strip)
-
- names.each do |name|
- name.slice! '* '
- end
+ def branch_names_contains(sha)
+ refs_contains_sha('branch', sha)
+ end
- names
- else
- []
- end
+ def tag_names_contains(sha)
+ refs_contains_sha('tag', sha)
end
def branches
@@ -468,7 +578,7 @@ class Repository
root_ref_commit = commit(root_ref)
if branch_commit
- rugged.merge_base(root_ref_commit.id, branch_commit.id) == branch_commit.id
+ is_ancestor?(branch_commit.id, root_ref_commit.id)
else
nil
end
@@ -478,9 +588,14 @@ class Repository
rugged.merge_base(first_commit_id, second_commit_id)
end
+ def is_ancestor?(ancestor_id, descendant_id)
+ merge_base(ancestor_id, descendant_id) == ancestor_id
+ end
+
+
def search_files(query, ref)
offset = 2
- args = %W(git grep -i -n --before-context #{offset} --after-context #{offset} #{query} #{ref || root_ref})
+ args = %W(#{Gitlab.config.git.bin_path} grep -i -n --before-context #{offset} --after-context #{offset} -e #{query} #{ref || root_ref})
Gitlab::Popen.popen(args, path_to_repo).first.scrub.split(/^--$/)
end
@@ -512,60 +627,57 @@ class Repository
end
def fetch_ref(source_path, source_ref, target_ref)
- args = %W(git fetch #{source_path} #{source_ref}:#{target_ref})
+ args = %W(#{Gitlab.config.git.bin_path} fetch -f #{source_path} #{source_ref}:#{target_ref})
Gitlab::Popen.popen(args, path_to_repo)
end
- def commit_with_hooks(current_user, branch)
- oldrev = Gitlab::Git::BLANK_SHA
- ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
- gl_id = Gitlab::ShellEnv.gl_id(current_user)
- was_empty = empty?
-
- # Create temporary ref
+ def with_tmp_ref(oldrev = nil)
random_string = SecureRandom.hex
tmp_ref = "refs/tmp/#{random_string}/head"
- unless was_empty
- oldrev = find_branch(branch).target
+ if oldrev && !Gitlab::Git.blank_ref?(oldrev)
rugged.references.create(tmp_ref, oldrev)
end
# Make commit in tmp ref
- newrev = yield(tmp_ref)
+ yield(tmp_ref)
+ ensure
+ rugged.references.delete(tmp_ref) rescue nil
+ end
- unless newrev
- raise CommitError.new('Failed to create commit')
+ def commit_with_hooks(current_user, branch)
+ oldrev = Gitlab::Git::BLANK_SHA
+ ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
+ was_empty = empty?
+
+ unless was_empty
+ oldrev = find_branch(branch).target
end
- # Run GitLab pre-receive hook
- pre_receive_hook = Gitlab::Git::Hook.new('pre-receive', path_to_repo)
- status = pre_receive_hook.trigger(gl_id, oldrev, newrev, ref)
+ with_tmp_ref(oldrev) do |tmp_ref|
+ # Make commit in tmp ref
+ newrev = yield(tmp_ref)
- if status
- if was_empty
- # Create branch
- rugged.references.create(ref, newrev)
- else
- # Update head
- current_head = find_branch(branch).target
+ unless newrev
+ raise CommitError.new('Failed to create commit')
+ end
- # Make sure target branch was not changed during pre-receive hook
- if current_head == oldrev
- rugged.references.update(ref, newrev)
+ GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
+ if was_empty
+ # Create branch
+ rugged.references.create(ref, newrev)
else
- raise CommitError.new('Commit was rejected because branch received new push')
+ # Update head
+ current_head = find_branch(branch).target
+
+ # Make sure target branch was not changed during pre-receive hook
+ if current_head == oldrev
+ rugged.references.update(ref, newrev)
+ else
+ raise CommitError.new('Commit was rejected because branch received new push')
+ end
end
end
-
- # Run GitLab post receive hook
- post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
- post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
- else
- # Remove tmp ref and return error to user
- rugged.references.delete(tmp_ref)
-
- raise PreReceiveError.new('Commit was rejected by pre-receive hook')
end
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 3eed5c16e45..f36eda1531b 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -17,12 +17,11 @@ class SentNotification < ActiveRecord::Base
belongs_to :noteable, polymorphic: true
belongs_to :recipient, class_name: "User"
- validate :project, :recipient, :reply_key, presence: true
- validate :reply_key, uniqueness: true
-
+ validates :project, :recipient, :reply_key, presence: true
+ validates :reply_key, uniqueness: true
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
+ validates :line_code, line_code: true, allow_blank: true
class << self
def reply_key
diff --git a/app/models/service.rb b/app/models/service.rb
index d610abd1683..d3bf7f0ebd1 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -30,6 +30,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
+ default_value_for :build_events, true
after_initialize :initialize_properties
@@ -40,13 +41,14 @@ class Service < ActiveRecord::Base
validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
- scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
+ scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) }
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
+ scope :build_hooks, -> { where(build_events: true, active: true) }
def activated?
active
@@ -133,6 +135,21 @@ class Service < ActiveRecord::Base
end
end
+ # Provide convenient boolean accessor methods
+ # for each serialized property.
+ # Also keep track of updated properties in a similar way as ActiveModel::Dirty
+ def self.boolean_accessor(*args)
+ self.prop_accessor(*args)
+
+ args.each do |arg|
+ class_eval %{
+ def #{arg}?
+ ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(#{arg})
+ end
+ }
+ end
+ end
+
# Returns a hash of the properties that have been assigned a new value since last save,
# indicating their original values (attr => original value).
# ActiveRecord does not provide a mechanism to track changes in serialized keys,
@@ -163,6 +180,7 @@ class Service < ActiveRecord::Base
assembla
bamboo
buildkite
+ builds_email
campfire
custom_issue_tracker
drone_ci
@@ -170,7 +188,6 @@ class Service < ActiveRecord::Base
external_wiki
flowdock
gemnasium
- gitlab_ci
hipchat
irker
jira
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index b0831982aa7..f876be7a4c8 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -65,6 +65,10 @@ class Snippet < ActiveRecord::Base
}x
end
+ def self.link_reference_pattern
+ super("snippets", /(?<snippet>\d+)/)
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{id}"
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 93b3246a668..e0e04d8859f 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -17,18 +17,16 @@ class Tree
def readme
return @readme if defined?(@readme)
- available_readmes = blobs.select(&:readme?)
+ # Take the first previewable readme, or return nil if none is available or
+ # we can't preview any of them
+ readme_tree = blobs.find do |blob|
+ blob.readme? && (previewable?(blob.name) || plain?(blob.name))
+ end
- if available_readmes.count == 0
+ if readme_tree.nil?
return @readme = nil
end
- # Take the first previewable readme, or the first available readme, if we
- # can't preview any of them
- readme_tree = available_readmes.find do |readme|
- previewable?(readme.name)
- end || available_readmes.first
-
readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
git_repo = repository.raw_repository
diff --git a/app/models/user.rb b/app/models/user.rb
index 7e4321d5376..20f907e4347 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -26,6 +26,7 @@
# bio :string(255)
# failed_attempts :integer default(0)
# locked_at :datetime
+# unlock_token :string(255)
# username :string(255)
# can_create_group :boolean default(TRUE), not null
# can_create_team :boolean default(TRUE), not null
@@ -54,7 +55,9 @@
# public_email :string(255) default(""), not null
# dashboard :integer default(0)
# project_view :integer default(0)
+# consumed_timestep :integer
# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -67,8 +70,10 @@ class User < ActiveRecord::Base
include Gitlab::CurrentSettings
include Referable
include Sortable
- include TokenAuthenticatable
include CaseSensitivity
+ include TokenAuthenticatable
+
+ add_authentication_token_field :authentication_token
default_value_for :admin, false
default_value_for :can_create_group, gitlab_config.default_can_create_group
@@ -132,7 +137,7 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
has_one :abuse_report, dependent: :destroy
- has_many :ci_builds, dependent: :nullify, class_name: 'Ci::Build'
+ has_many :builds, dependent: :nullify, class_name: 'Ci::Build'
#
@@ -147,11 +152,9 @@ class User < ActiveRecord::Base
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username,
+ namespace: true,
presence: true,
- uniqueness: { case_sensitive: false },
- exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.namespace_regex,
- message: Gitlab::Regex.namespace_regex_message }
+ uniqueness: { case_sensitive: false }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
@@ -218,9 +221,9 @@ class User < ActiveRecord::Base
def find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
- where(conditions).where(["lower(username) = :value OR lower(email) = :value", { value: login.downcase }]).first
+ where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase)
else
- where(conditions).first
+ find_by(conditions)
end
end
@@ -235,21 +238,16 @@ class User < ActiveRecord::Base
# Find a User by their primary email or any associated secondary email
def find_by_any_email(email)
- user_table = arel_table
- email_table = Email.arel_table
-
- # Use ARel to build a query:
- query = user_table.
- # SELECT "users".* FROM "users"
- project(user_table[Arel.star]).
- # LEFT OUTER JOIN "emails"
- join(email_table, Arel::Nodes::OuterJoin).
- # ON "users"."id" = "emails"."user_id"
- on(user_table[:id].eq(email_table[:user_id])).
- # WHERE ("user"."email" = '<email>' OR "emails"."email" = '<email>')
- where(user_table[:email].eq(email).or(email_table[:email].eq(email)))
-
- find_by_sql(query.to_sql).first
+ sql = 'SELECT *
+ FROM users
+ WHERE id IN (
+ SELECT id FROM users WHERE email = :email
+ UNION
+ SELECT emails.user_id FROM emails WHERE email = :email
+ )
+ LIMIT 1;'
+
+ User.find_by_sql([sql, { email: email }]).first
end
def filter(filter_name)
@@ -288,7 +286,7 @@ class User < ActiveRecord::Base
end
def by_username_or_id(name_or_id)
- where('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i).first
+ find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
def build_user(attrs = {})
@@ -354,10 +352,13 @@ class User < ActiveRecord::Base
end
def namespace_uniq
+ # Return early if username already failed the first uniqueness validation
+ return if self.errors[:username].include?('has already been taken')
+
namespace_name = self.username
existing_namespace = Namespace.by_path(namespace_name)
if existing_namespace && existing_namespace != self.namespace
- self.errors.add :username, "already exists"
+ self.errors.add(:username, 'has already been taken')
end
end
@@ -393,31 +394,23 @@ class User < ActiveRecord::Base
end
end
- # Groups user has access to
+ # Returns the groups a user has access to
def authorized_groups
- @authorized_groups ||= begin
- group_ids = (groups.pluck(:id) + authorized_projects.pluck(:namespace_id))
- Group.where(id: group_ids)
- end
- end
+ union = Gitlab::SQL::Union.
+ new([groups.select(:id), authorized_projects.select(:namespace_id)])
+ Group.where("namespaces.id IN (#{union.to_sql})")
+ end
- # Projects user has access to
+ # Returns the groups a user is authorized to access.
def authorized_projects
- @authorized_projects ||= begin
- project_ids = personal_projects.pluck(:id)
- project_ids.push(*groups_projects.pluck(:id))
- project_ids.push(*projects.pluck(:id).uniq)
- Project.where(id: project_ids)
- end
+ Project.where("projects.id IN (#{projects_union.to_sql})")
end
def owned_projects
@owned_projects ||=
- begin
- namespace_ids = owned_groups.pluck(:id).push(namespace.id)
- Project.in_namespace(namespace_ids).joins(:namespace)
- end
+ Project.where('namespace_id IN (?) OR namespace_id = ?',
+ owned_groups.select(:id), namespace.id).joins(:namespace)
end
# Team membership in authorized projects
@@ -649,11 +642,11 @@ class User < ActiveRecord::Base
email.start_with?('temp-email-for-oauth')
end
- def avatar_url(size = nil)
+ def avatar_url(size = nil, scale = 2)
if avatar.present?
[gitlab_config.url, avatar.url].join
else
- GravatarService.new.execute(email, size)
+ GravatarService.new.execute(email, size, scale)
end
end
@@ -702,7 +695,7 @@ class User < ActiveRecord::Base
end
def starred?(project)
- starred_projects.exists?(project)
+ starred_projects.exists?(project.id)
end
def toggle_star(project)
@@ -732,12 +725,25 @@ class User < ActiveRecord::Base
Doorkeeper::AccessToken.where(resource_owner_id: self.id, revoked_at: nil)
end
- def contributed_projects_ids
- Event.contributions.where(author_id: self).
+ # Returns the projects a user contributed to in the last year.
+ #
+ # This method relies on a subquery as this performs significantly better
+ # compared to a JOIN when coupled with, for example,
+ # `Project.visible_to_user`. That is, consider the following code:
+ #
+ # some_user.contributed_projects.visible_to_user(other_user)
+ #
+ # If this method were to use a JOIN the resulting query would take roughly 200
+ # ms on a database with a similar size to GitLab.com's database. On the other
+ # hand, using a subquery means we can get the exact same data in about 40 ms.
+ def contributed_projects
+ events = Event.select(:project_id).
+ contributions.where(author_id: self).
where("created_at > ?", Time.now - 1.year).
- reorder(project_id: :desc).
- select(:project_id).
- uniq.map(&:project_id)
+ uniq.
+ reorder(nil)
+
+ Project.where(id: events)
end
def restricted_signup_domains
@@ -767,12 +773,34 @@ class User < ActiveRecord::Base
!solo_owned_groups.present?
end
- def ci_authorized_projects
- @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects)
+ def ci_authorized_runners
+ @ci_authorized_runners ||= begin
+ runner_ids = Ci::RunnerProject.
+ where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+ select(:runner_id)
+ Ci::Runner.specific.where(id: runner_ids)
+ end
end
- def ci_authorized_runners
- Ci::Runner.specific.includes(:runner_projects).
- where(ci_runner_projects: { project_id: ci_authorized_projects } )
+ private
+
+ def projects_union
+ Gitlab::SQL::Union.new([personal_projects.select(:id),
+ groups_projects.select(:id),
+ projects.select(:id)])
+ end
+
+ def ci_projects_union
+ scope = { access_level: [Gitlab::Access::MASTER, Gitlab::Access::OWNER] }
+ groups = groups_projects.where(members: scope)
+ other = projects.where(members: scope)
+
+ Gitlab::SQL::Union.new([personal_projects.select(:id), groups.select(:id),
+ other.select(:id)])
+ end
+
+ # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration
+ def send_devise_notification(notification, *args)
+ devise_mailer.send(notification, self, *args).deliver_later
end
end
diff --git a/app/models/users_star_project.rb b/app/models/users_star_project.rb
index 3d49cb05949..413f3f485a8 100644
--- a/app/models/users_star_project.rb
+++ b/app/models/users_star_project.rb
@@ -10,7 +10,7 @@
#
class UsersStarProject < ActiveRecord::Base
- belongs_to :project, counter_cache: :star_count
+ belongs_to :project, counter_cache: :star_count, touch: true
belongs_to :user
validates :user, presence: true
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index f00ec7408b6..b48ca67d4d2 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -39,10 +39,7 @@ class BaseService
def deny_visibility_level(model, denied_visibility_level = nil)
denied_visibility_level ||= model.visibility_level
- level_name = 'Unknown'
- Gitlab::VisibilityLevel.options.each do |name, level|
- level_name = name if level == denied_visibility_level
- end
+ level_name = Gitlab::VisibilityLevel.level_name(denied_visibility_level)
model.errors.add(
:visibility_level,
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
index 912eb6258a4..ad901f2da5d 100644
--- a/app/services/ci/create_builds_service.rb
+++ b/app/services/ci/create_builds_service.rb
@@ -29,9 +29,11 @@ module Ci
build_attrs.merge!(ref: ref,
tag: tag,
trigger_request: trigger_request,
- user: user)
+ user: user,
+ project: commit.project)
- commit.builds.create!(build_attrs)
+ build = commit.builds.create!(build_attrs)
+ build.execute_hooks
end
end
end
diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb
deleted file mode 100644
index 479a2d6defc..00000000000
--- a/app/services/ci/create_commit_service.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module Ci
- class CreateCommitService
- def execute(project, user, params)
- sha = params[:checkout_sha] || params[:after]
- origin_ref = params[:ref]
-
- unless origin_ref && sha.present?
- return false
- end
-
- ref = origin_ref.gsub(/\Arefs\/(tags|heads)\//, '')
-
- # Skip branch removal
- if sha == Ci::Git::BLANK_SHA
- return false
- end
-
- tag = origin_ref.start_with?('refs/tags/')
- commit = project.gl_project.ensure_ci_commit(sha)
- unless commit.skip_ci?
- commit.update_committed!
- commit.create_builds(ref, tag, user)
- end
-
- commit
- end
- end
-end
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index 4b86cb0a1f5..b3dfc707221 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,13 +1,13 @@
module Ci
class CreateTriggerRequestService
def execute(project, trigger, ref, variables = nil)
- commit = project.gl_project.commit(ref)
+ commit = project.commit(ref)
return unless commit
# check if ref is tag
- tag = project.gl_project.repository.find_tag(ref).present?
+ tag = project.repository.find_tag(ref).present?
- ci_commit = project.gl_project.ensure_ci_commit(commit.sha)
+ ci_commit = project.ensure_ci_commit(commit.sha)
trigger_request = trigger.trigger_requests.create!(
variables: variables,
diff --git a/app/services/ci/event_service.rb b/app/services/ci/event_service.rb
deleted file mode 100644
index 3f4e02dd26c..00000000000
--- a/app/services/ci/event_service.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-module Ci
- class EventService
- def remove_project(user, project)
- create(
- description: "Project \"#{project.name}\" has been removed by #{user.username}",
- user_id: user.id,
- is_admin: true
- )
- end
-
- def create_project(user, project)
- create(
- description: "Project \"#{project.name}\" has been created by #{user.username}",
- user_id: user.id,
- is_admin: true
- )
- end
-
- def change_project_settings(user, project)
- create(
- project_id: project.id,
- user_id: user.id,
- description: "User \"#{user.username}\" updated projects settings"
- )
- end
-
- def create(*args)
- Ci::Event.create!(*args)
- end
- end
-end
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index b95835ba093..f469b13e902 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,17 +1,15 @@
module Ci
class ImageForBuildService
def execute(project, params)
- image_name =
- if params[:sha]
- commit = project.commits.find_by(sha: params[:sha])
- image_for_commit(commit)
- elsif params[:ref]
- commit = project.last_commit_for_ref(params[:ref])
- image_for_commit(commit)
- else
- 'build-unknown.svg'
+ sha = params[:sha]
+ sha ||=
+ if params[:ref]
+ project.commit(params[:ref]).try(:sha)
end
+ commit = project.ci_commits.ordered.find_by(sha: sha)
+ image_name = image_for_commit(commit)
+
image_path = Rails.root.join('public/ci', image_name)
OpenStruct.new(
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 7beb098659c..4ff268a6f06 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -8,10 +8,10 @@ module Ci
builds =
if current_runner.shared?
# don't run projects which have not enables shared runners
- builds.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true })
+ builds.joins(:project).where(projects: { builds_enabled: true, shared_runners_enabled: true })
else
# do run projects which are only assigned to this runner
- builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids })
+ builds.where(project: current_runner.projects.where(builds_enabled: true))
end
builds = builds.order('created_at ASC')
@@ -20,10 +20,9 @@ module Ci
build.can_be_served?(current_runner)
end
-
if build
# In case when 2 runners try to assign the same build, second runner will be declined
- # with StateMachine::InvalidTransition in run! method.
+ # with StateMachines::InvalidTransition in run! method.
build.with_lock do
build.runner_id = current_runner.id
build.save!
@@ -33,7 +32,7 @@ module Ci
build
- rescue StateMachine::InvalidTransition
+ rescue StateMachines::InvalidTransition
nil
end
end
diff --git a/app/services/ci/test_hook_service.rb b/app/services/ci/test_hook_service.rb
deleted file mode 100644
index 3a17596aaeb..00000000000
--- a/app/services/ci/test_hook_service.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Ci
- class TestHookService
- def execute(hook, current_user)
- Ci::WebHookService.new.build_end(hook.project.commits.last.last_build)
- end
- end
-end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index bfe6a3dc4be..ec581658fc1 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -3,7 +3,7 @@ require 'securerandom'
# Compare 2 branches for one repo or between repositories
# and return Gitlab::CompareResult object that responds to commits and diffs
class CompareService
- def execute(source_project, source_branch, target_project, target_branch)
+ def execute(source_project, source_branch, target_project, target_branch, diff_options = {})
source_commit = source_project.commit(source_branch)
return unless source_commit
@@ -25,7 +25,7 @@ class CompareService
target_project.repository.raw_repository,
target_branch,
source_sha,
- )
+ ), diff_options
)
end
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index cf7ae4345f3..f139872c728 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -1,10 +1,10 @@
require_relative 'base_service'
class CreateBranchService < BaseService
- def execute(branch_name, ref)
+ def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
if valid_branch == false
- return error('Branch name invalid')
+ return error('Branch name is invalid')
end
repository = project.repository
@@ -13,8 +13,20 @@ class CreateBranchService < BaseService
return error('Branch already exists')
end
- repository.add_branch(branch_name, ref)
- new_branch = repository.find_branch(branch_name)
+ new_branch = nil
+ if source_project != @project
+ repository.with_tmp_ref do |tmp_ref|
+ repository.fetch_ref(
+ source_project.repository.path_to_repo,
+ "refs/heads/#{ref}",
+ tmp_ref
+ )
+
+ new_branch = repository.add_branch(current_user, branch_name, tmp_ref)
+ end
+ else
+ new_branch = repository.add_branch(current_user, branch_name, ref)
+ end
if new_branch
push_data = build_push_data(project, current_user, new_branch)
@@ -27,6 +39,8 @@ class CreateBranchService < BaseService
else
error('Invalid reference name')
end
+ rescue GitHooksService::PreReceiveError
+ error('Branch creation was rejected by Git hook')
end
def success(branch)
diff --git a/app/services/create_commit_builds_service.rb b/app/services/create_commit_builds_service.rb
new file mode 100644
index 00000000000..31b407efeb1
--- /dev/null
+++ b/app/services/create_commit_builds_service.rb
@@ -0,0 +1,42 @@
+class CreateCommitBuildsService
+ def execute(project, user, params)
+ return false unless project.builds_enabled?
+
+ 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)
+
+ # Skip branch removal
+ if sha == Gitlab::Git::BLANK_SHA
+ return false
+ end
+
+ commit = project.ci_commit(sha)
+ unless commit
+ commit = project.ci_commits.new(sha: sha)
+
+ # Skip creating ci_commit when no gitlab-ci.yml is found
+ unless commit.ci_yaml_file
+ return false
+ end
+
+ # Create a new ci_commit
+ commit.save!
+ end
+
+ # Skip creating builds for commits that have [ci skip]
+ unless commit.skip_ci?
+ # Create builds for commit
+ tag = Gitlab::Git.tag_ref?(origin_ref)
+ commit.update_committed!
+ commit.create_builds(ref, tag, user)
+ end
+
+ commit
+ end
+end
diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb
new file mode 100644
index 00000000000..e06a6f2f47a
--- /dev/null
+++ b/app/services/create_release_service.rb
@@ -0,0 +1,31 @@
+require_relative 'base_service'
+
+class CreateReleaseService < BaseService
+ def execute(tag_name, release_description)
+
+ repository = project.repository
+ existing_tag = repository.find_tag(tag_name)
+
+ # Only create a release if the tag exists
+ if existing_tag
+ release = project.releases.find_by(tag: tag_name)
+
+ if release
+ error('Release already exists', 409)
+ else
+ release = project.releases.new({ tag: tag_name, description: release_description })
+ release.save
+
+ success(release)
+ end
+ else
+ error('Tag does not exist', 404)
+ end
+ end
+
+ def success(release)
+ out = super()
+ out[:release] = release
+ out
+ end
+end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 1a7318048b3..2452999382a 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -1,7 +1,7 @@
require_relative 'base_service'
class CreateTagService < BaseService
- def execute(tag_name, ref, message)
+ def execute(tag_name, ref, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
if valid_tag == false
return error('Tag name invalid')
@@ -20,11 +20,15 @@ class CreateTagService < BaseService
if new_tag
push_data = create_push_data(project, current_user, new_tag)
-
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
+ if release_description
+ CreateReleaseService.new(@project, @current_user).
+ execute(tag_name, release_description)
+ end
+
success(new_tag)
else
error('Invalid reference name')
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index b19b112a0c4..22bf9dd935e 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -24,7 +24,7 @@ class DeleteBranchService < BaseService
return error('You dont have push access to repo', 405)
end
- if repository.rm_branch(branch_name)
+ if repository.rm_branch(current_user, branch_name)
push_data = build_push_data(branch)
EventCreateService.new.push(project, current_user, push_data)
@@ -35,6 +35,8 @@ class DeleteBranchService < BaseService
else
error('Failed to remove branch')
end
+ rescue GitHooksService::PreReceiveError
+ error('Branch deletion was rejected by Git hook')
end
def error(message, return_code = 400)
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index 0c836401136..de3352a6756 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -11,8 +11,10 @@ class DeleteTagService < BaseService
end
if repository.rm_tag(tag_name)
+ release = project.releases.find_by(tag: tag_name)
+ release.destroy if release
+
push_data = build_push_data(tag)
-
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 008833eed80..0326a8823e9 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -3,8 +3,10 @@ module Files
class ValidationError < StandardError; end
def execute
- @current_branch = params[:current_branch]
+ @source_project = params[:source_project] || @project
+ @source_branch = params[:source_branch]
@target_branch = params[:target_branch]
+
@commit_message = params[:commit_message]
@file_path = params[:file_path]
@file_content = if params[:file_content_encoding] == 'base64'
@@ -16,8 +18,8 @@ module Files
# Validate parameters
validate
- # Create new branch if it different from current_branch
- if @target_branch != @current_branch
+ # Create new branch if it different from source_branch
+ if different_branch?
create_target_branch
end
@@ -26,18 +28,14 @@ module Files
else
error("Something went wrong. Your changes were not committed")
end
- rescue Repository::CommitError, Repository::PreReceiveError, ValidationError => ex
+ rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError, ValidationError => ex
error(ex.message)
end
private
- def current_branch
- @current_branch ||= params[:current_branch]
- end
-
- def target_branch
- @target_branch ||= params[:target_branch]
+ def different_branch?
+ @source_branch != @target_branch || @source_project != @project
end
def raise_error(message)
@@ -52,11 +50,11 @@ module Files
end
unless project.empty_repo?
- unless repository.branch_names.include?(@current_branch)
- raise_error("You can only create files if you are on top of a branch")
+ unless @source_project.repository.branch_names.include?(@source_branch)
+ raise_error("You can only create or edit files when you are on a branch")
end
- if @current_branch != @target_branch
+ 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")
end
@@ -65,10 +63,10 @@ module Files
end
def create_target_branch
- result = CreateBranchService.new(project, current_user).execute(@target_branch, @current_branch)
+ result = CreateBranchService.new(project, current_user).execute(@target_branch, @source_branch, source_project: @source_project)
unless result[:status] == :success
- raise_error("Something went wrong when we tried to create #{@target_branch} for you")
+ raise_error("Something went wrong when we tried to create #{@target_branch} for you: #{result[:message]}")
end
end
end
diff --git a/app/services/files/create_dir_service.rb b/app/services/files/create_dir_service.rb
index 71272fb5707..6107254a34e 100644
--- a/app/services/files/create_dir_service.rb
+++ b/app/services/files/create_dir_service.rb
@@ -5,5 +5,16 @@ module Files
def commit
repository.commit_dir(current_user, @file_path, @commit_message, @target_branch)
end
+
+ def validate
+ super
+
+ unless @file_path =~ Gitlab::Regex.file_path_regex
+ raise_error(
+ 'Your changes could not be committed, because the file path ' +
+ Gitlab::Regex.file_path_regex_message
+ )
+ end
+ end
end
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index c8e3a910bba..e4cde4a2fd8 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -9,19 +9,24 @@ module Files
def validate
super
- file_name = File.basename(@file_path)
+ if @file_path =~ Gitlab::Regex.directory_traversal_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name ' +
+ Gitlab::Regex.directory_traversal_regex_message
+ )
+ end
- unless file_name =~ Gitlab::Regex.file_name_regex
+ unless @file_path =~ Gitlab::Regex.file_path_regex
raise_error(
'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.file_name_regex_message
+ Gitlab::Regex.file_path_regex_message
)
end
unless project.empty_repo?
@file_path.slice!(0) if @file_path.start_with?('/')
- blob = repository.blob_at_branch(@current_branch, @file_path)
+ 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")
diff --git a/app/services/git_hooks_service.rb b/app/services/git_hooks_service.rb
new file mode 100644
index 00000000000..8f5c3393dfc
--- /dev/null
+++ b/app/services/git_hooks_service.rb
@@ -0,0 +1,28 @@
+class GitHooksService
+ PreReceiveError = Class.new(StandardError)
+
+ def execute(user, repo_path, oldrev, newrev, ref)
+ @repo_path = repo_path
+ @user = Gitlab::ShellEnv.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")
+ end
+ end
+
+ yield
+
+ run_hook('post-receive')
+ end
+
+ private
+
+ def run_hook(name)
+ hook = Gitlab::Git::Hook.new(name, @repo_path)
+ hook.trigger(@user, @oldrev, @newrev, @ref)
+ end
+end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 3de7bb9dcaa..d7ea30bc315 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -58,15 +58,10 @@ class GitPushService
@push_data = build_push_data(oldrev, newrev, ref)
- # If CI was disabled but .gitlab-ci.yml file was pushed
- # we enable CI automatically
- if !project.gitlab_ci? && gitlab_ci_yaml?(newrev)
- project.enable_ci
- end
-
EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup, :push_hooks)
+ CreateCommitBuildsService.new.execute(project, @user, @push_data)
ProjectCacheWorker.perform_async(project.id)
end
@@ -134,10 +129,4 @@ class GitPushService
def commit_user(commit)
commit.author || user
end
-
- def gitlab_ci_yaml?(sha)
- @project.repository.blob_at(sha, '.gitlab-ci.yml')
- rescue Rugged::ReferenceError
- nil
- end
end
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index 1cc42b0b0ad..4144c7111d0 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -10,6 +10,7 @@ class GitTagPushService
EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
+ CreateCommitBuildsService.new.execute(project, @user, @push_data)
ProjectCacheWorker.perform_async(project.id)
true
diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb
index 4bee0c26a68..433ecc2df32 100644
--- a/app/services/gravatar_service.rb
+++ b/app/services/gravatar_service.rb
@@ -1,13 +1,13 @@
class GravatarService
include Gitlab::CurrentSettings
- def execute(email, size = nil)
+ def execute(email, size = nil, scale = 2)
if current_application_settings.gravatar_enabled? && email.present?
size = 40 if size.nil? || size <= 0
sprintf gravatar_url,
hash: Digest::MD5.hexdigest(email.strip.downcase),
- size: size,
+ size: size * scale,
email: email.strip
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 15b3825f96a..2556f06e2d3 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -27,7 +27,16 @@ class IssuableBaseService < BaseService
old_branch, new_branch)
end
+ def create_task_status_note(issuable)
+ issuable.updated_tasks.each do |task|
+ SystemNoteService.change_task_status(issuable, issuable.project, current_user, task)
+ end
+ end
+
def filter_params(issuable_ability_name = :issue)
+ params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
+ params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
+
ability = :"admin_#{issuable_ability_name}"
unless can?(current_user, ability, project)
@@ -36,4 +45,44 @@ class IssuableBaseService < BaseService
params.delete(:assignee_id)
end
end
+
+ def update(issuable)
+ change_state(issuable)
+ filter_params
+ old_labels = issuable.labels.to_a
+
+ if params.present? && issuable.update_attributes(params.merge(updated_by: current_user))
+ issuable.reset_events_cache
+ handle_common_system_notes(issuable, old_labels: old_labels)
+ handle_changes(issuable)
+ issuable.create_new_cross_references!(current_user)
+ execute_hooks(issuable, 'update')
+ end
+
+ issuable
+ end
+
+ def change_state(issuable)
+ case params.delete(:state_event)
+ when 'reopen'
+ reopen_service.new(project, current_user, {}).execute(issuable)
+ when 'close'
+ close_service.new(project, current_user, {}).execute(issuable)
+ end
+ end
+
+ def handle_common_system_notes(issuable, options = {})
+ if issuable.previous_changes.include?('title')
+ create_title_change_note(issuable, issuable.previous_changes['title'].first)
+ end
+
+ if issuable.previous_changes.include?('description') && issuable.tasks?
+ create_task_status_note(issuable)
+ end
+
+ old_labels = options[:old_labels]
+ if old_labels && (issuable.labels != old_labels)
+ create_labels_note(issuable, issuable.labels - old_labels, old_labels - issuable.labels)
+ end
+ end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 3d85f97b7e5..a1a20e47681 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -1,6 +1,11 @@
module Issues
class CloseService < Issues::BaseService
def execute(issue, commit = nil)
+ if project.jira_tracker? && project.jira_service.active
+ project.jira_service.execute(commit, issue)
+ return issue
+ end
+
if project.default_issues_tracker? && issue.close
event_service.close_issue(issue, current_user)
create_note(issue, commit)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 2b5426ad452..a55a04dd5e0 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -1,45 +1,26 @@
module Issues
class UpdateService < Issues::BaseService
def execute(issue)
- case params.delete(:state_event)
- when 'reopen'
- Issues::ReopenService.new(project, current_user, {}).execute(issue)
- when 'close'
- Issues::CloseService.new(project, current_user, {}).execute(issue)
- end
-
- params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
- params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
-
- filter_params
- old_labels = issue.labels.to_a
-
- if params.present? && issue.update_attributes(params.merge(updated_by: current_user))
- issue.reset_events_cache
-
- if issue.labels != old_labels
- create_labels_note(
- issue, issue.labels - old_labels, old_labels - issue.labels)
- end
-
- if issue.previous_changes.include?('milestone_id')
- create_milestone_note(issue)
- end
-
- if issue.previous_changes.include?('assignee_id')
- create_assignee_note(issue)
- notification_service.reassigned_issue(issue, current_user)
- end
+ update(issue)
+ end
- if issue.previous_changes.include?('title')
- create_title_change_note(issue, issue.previous_changes['title'].first)
- end
+ def handle_changes(issue)
+ if issue.previous_changes.include?('milestone_id')
+ create_milestone_note(issue)
+ end
- issue.create_new_cross_references!
- execute_hooks(issue, 'update')
+ if issue.previous_changes.include?('assignee_id')
+ create_assignee_note(issue)
+ notification_service.reassigned_issue(issue, current_user)
end
+ end
+
+ def reopen_service
+ Issues::ReopenService
+ end
- issue
+ def close_service
+ Issues::CloseService
end
end
end
diff --git a/app/services/labels/group_service.rb b/app/services/labels/group_service.rb
deleted file mode 100644
index b26cee24d56..00000000000
--- a/app/services/labels/group_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Labels
- class GroupService < ::BaseService
- def initialize(project_labels)
- @project_labels = project_labels.group_by(&:title)
- end
-
- def execute
- build(@project_labels)
- end
-
- def label(title)
- if title
- group_label = @project_labels[title].group_by(&:title)
- build(group_label).first
- else
- nil
- end
- end
-
- private
-
- def build(label)
- label.map { |title, labels| GroupLabel.new(title, labels) }
- end
- end
-end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 7963af127e1..cabc3d8fabb 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -6,15 +6,12 @@ module MergeRequests
# Executed when you do merge via GitLab UI
#
class MergeService < MergeRequests::BaseService
- attr_reader :merge_request, :commit_message
+ attr_reader :merge_request
- def execute(merge_request, commit_message)
- @commit_message = commit_message
+ def execute(merge_request)
@merge_request = merge_request
- unless @merge_request.mergeable?
- return error('Merge request is not mergeable')
- end
+ return error('Merge request is not mergeable') unless @merge_request.mergeable?
merge_request.in_locked_state do
if commit
@@ -32,13 +29,13 @@ module MergeRequests
committer = repository.user_to_committer(current_user)
options = {
- message: commit_message,
+ message: params[:commit_message] || merge_request.merge_commit_message,
author: committer,
committer: committer
}
repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
- rescue Exception => e
+ rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge")
Rails.logger.error(e.message)
return false
@@ -46,6 +43,11 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
+
+ if params[:should_remove_source_branch]
+ DeleteBranchService.new(@merge_request.source_project, current_user).
+ execute(merge_request.source_branch)
+ end
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
new file mode 100644
index 00000000000..5cf7404a493
--- /dev/null
+++ b/app/services/merge_requests/merge_when_build_succeeds_service.rb
@@ -0,0 +1,55 @@
+module MergeRequests
+ class MergeWhenBuildSucceedsService < MergeRequests::BaseService
+ # Marks the passed `merge_request` to be merged when the build succeeds or
+ # updates the params for the automatic merge
+ def execute(merge_request)
+ merge_request.merge_params.merge!(params)
+
+ # The service is also called when the merge params are updated.
+ already_approved = merge_request.merge_when_build_succeeds?
+
+ unless already_approved
+ 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)
+ end
+
+ merge_request.save
+ end
+
+ # Triggers the automatic merge of merge_request once the build succeeds
+ def trigger(build)
+ merge_requests = merge_request_from(build)
+
+ merge_requests.each do |merge_request|
+ next unless merge_request.merge_when_build_succeeds?
+
+ if merge_request.ci_commit && merge_request.ci_commit.success? && merge_request.mergeable?
+ MergeWorker.perform_async(merge_request.id, merge_request.merge_user_id, merge_request.merge_params)
+ end
+ end
+ end
+
+ # Cancels the automatic merge
+ def cancel(merge_request)
+ if merge_request.merge_when_build_succeeds? && merge_request.open?
+ merge_request.reset_merge_when_build_succeeds
+ SystemNoteService.cancel_merge_when_build_succeeds(merge_request, @project, @current_user)
+
+ success
+ else
+ error("Can't cancel the automatic merge", 406)
+ end
+ end
+
+ private
+
+ def merge_request_from(build)
+ merge_requests = @project.origin_merge_requests.opened.where(source_branch: build.ref).to_a
+ merge_requests += @project.fork_merge_requests.opened.where(source_branch: build.ref).to_a
+
+ merge_requests.uniq.select(&:source_project)
+ end
+ end
+end
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index 121f6899011..8b3d56c2b4c 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -5,20 +5,20 @@ module MergeRequests
@oldrev, @newrev = oldrev, newrev
@branch_name = Gitlab::Git.ref_name(ref)
- @fork_merge_requests = @project.fork_merge_requests.opened
- @commits = []
- # Leave a system note if a branch were deleted/added
- if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
+ find_new_commits
+ # Be sure to close outstanding MRs before reloading them to avoid generating an
+ # empty diff during a manual merge
+ close_merge_requests
+ reload_merge_requests
+ reset_merge_when_build_succeeds
+
+ # Leave a system note if a branch was deleted/added
+ if branch_added? || branch_removed?
comment_mr_branch_presence_changed
- comment_mr_with_commits if @commits.present?
- else
- @commits = @project.repository.commits_between(oldrev, newrev)
- comment_mr_with_commits
- close_merge_requests
end
- reload_merge_requests
+ comment_mr_with_commits
execute_mr_web_hooks
true
@@ -54,11 +54,10 @@ module MergeRequests
# Note: we should update merge requests from forks too
def reload_merge_requests
merge_requests = @project.merge_requests.opened.by_branch(@branch_name).to_a
- merge_requests += @fork_merge_requests.by_branch(@branch_name).to_a
+ merge_requests += fork_merge_requests.by_branch(@branch_name).to_a
merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request|
-
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_code
merge_request.mark_as_unchecked
@@ -77,37 +76,51 @@ module MergeRequests
end
end
- # Add comment about branches being deleted or added to merge requests
- def comment_mr_branch_presence_changed
- presence = Gitlab::Git.blank_ref?(@oldrev) ? :add : :delete
+ def reset_merge_when_build_succeeds
+ merge_requests_for_source_branch.each(&:reset_merge_when_build_succeeds)
+ end
- merge_requests_for_source_branch.each do |merge_request|
- last_commit = merge_request.last_commit
+ def find_new_commits
+ if branch_added?
+ @commits = []
- # Only look at changed commits in restore branch case
- unless Gitlab::Git.blank_ref?(@newrev)
- 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)
- # 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
- rescue
- end
+ merge_request = merge_requests_for_source_branch.first
+ return unless merge_request
- # Prevent system notes from seeing a blank SHA
- @oldrev = nil
+ 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)
+ # 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
+ rescue
end
+ elsif branch_removed?
+ # No commits for a deleted branch.
+ @commits = []
+ else
+ @commits = @project.repository.commits_between(@oldrev, @newrev)
+ end
+ end
+ # Add comment about branches being deleted or added to merge requests
+ def comment_mr_branch_presence_changed
+ presence = branch_added? ? :add : :delete
+
+ merge_requests_for_source_branch.each do |merge_request|
SystemNoteService.change_branch_presence(
- merge_request, merge_request.project, @current_user,
+ merge_request, merge_request.project, @current_user,
:source, @branch_name, presence)
end
end
# Add comment about pushing new commits to merge requests
def comment_mr_with_commits
+ return unless @commits.present?
+
merge_requests_for_source_branch.each do |merge_request|
mr_commit_ids = Set.new(merge_request.commits.map(&:id))
@@ -135,9 +148,21 @@ module MergeRequests
def merge_requests_for_source_branch
@source_merge_requests ||= begin
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
- merge_requests += @fork_merge_requests.where(source_branch: @branch_name).to_a
+ merge_requests += fork_merge_requests.where(source_branch: @branch_name).to_a
filter_merge_requests(merge_requests)
end
end
+
+ def fork_merge_requests
+ @fork_merge_requests ||= @project.fork_merge_requests.opened
+ end
+
+ def branch_added?
+ Gitlab::Git.blank_ref?(@oldrev)
+ end
+
+ def branch_removed?
+ Gitlab::Git.blank_ref?(@newrev)
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index ebbe0af803b..5ff2cc03dda 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,59 +11,37 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
- case params.delete(:state_event)
- when 'reopen'
- MergeRequests::ReopenService.new(project, current_user, {}).execute(merge_request)
- when 'close'
- MergeRequests::CloseService.new(project, current_user, {}).execute(merge_request)
- end
-
- params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
- params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
-
- filter_params
- old_labels = merge_request.labels.to_a
-
- if params.present? && merge_request.update_attributes(params.merge(updated_by: current_user))
- merge_request.reset_events_cache
-
- if merge_request.labels != old_labels
- create_labels_note(
- merge_request,
- merge_request.labels - old_labels,
- old_labels - merge_request.labels
- )
- end
-
- if merge_request.previous_changes.include?('target_branch')
- create_branch_change_note(merge_request, 'target',
- merge_request.previous_changes['target_branch'].first,
- merge_request.target_branch)
- end
-
- if merge_request.previous_changes.include?('milestone_id')
- create_milestone_note(merge_request)
- end
+ update(merge_request)
+ end
- if merge_request.previous_changes.include?('assignee_id')
- create_assignee_note(merge_request)
- notification_service.reassigned_merge_request(merge_request, current_user)
- end
+ def handle_changes(merge_request)
+ if merge_request.previous_changes.include?('target_branch')
+ create_branch_change_note(merge_request, 'target',
+ merge_request.previous_changes['target_branch'].first,
+ merge_request.target_branch)
+ end
- if merge_request.previous_changes.include?('title')
- create_title_change_note(merge_request, merge_request.previous_changes['title'].first)
- end
+ if merge_request.previous_changes.include?('milestone_id')
+ create_milestone_note(merge_request)
+ end
- if merge_request.previous_changes.include?('target_branch') ||
- merge_request.previous_changes.include?('source_branch')
- merge_request.mark_as_unchecked
- end
+ if merge_request.previous_changes.include?('assignee_id')
+ create_assignee_note(merge_request)
+ notification_service.reassigned_merge_request(merge_request, current_user)
+ end
- merge_request.create_new_cross_references!
- execute_hooks(merge_request, 'update')
+ if merge_request.previous_changes.include?('target_branch') ||
+ merge_request.previous_changes.include?('source_branch')
+ merge_request.mark_as_unchecked
end
+ end
+
+ def reopen_service
+ MergeRequests::ReopenService
+ end
- merge_request
+ def close_service
+ MergeRequests::CloseService
end
end
end
diff --git a/app/services/milestones/group_service.rb b/app/services/milestones/group_service.rb
deleted file mode 100644
index 11d702f1e7b..00000000000
--- a/app/services/milestones/group_service.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module Milestones
- class GroupService < Milestones::BaseService
- def initialize(project_milestones)
- @project_milestones = project_milestones.group_by(&:title)
- end
-
- def execute
- build(@project_milestones)
- end
-
- def milestone(title)
- if title
- group_milestone = @project_milestones[title].group_by(&:title)
- build(group_milestone).first
- else
- nil
- end
- end
-
- private
-
- def build(milestone)
- milestone.map{ |title, milestones| GroupMilestone.new(title, milestones) }
- end
- end
-end
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 2001dc89c33..a8486e6a5a1 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -8,8 +8,8 @@ module Notes
if note.save
notification_service.new_note(note)
- # Skip system notes, like status changes and cross-references.
- unless note.system
+ # Skip system notes, like status changes and cross-references and awards
+ unless note.system || note.is_award
event_service.leave_note(note, note.author)
note.create_cross_references!
execute_hooks(note)
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 6c2f08e5963..72e2f78008d 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -4,7 +4,7 @@ module Notes
return note unless note.editable?
note.update_attributes(params.merge(updated_by: current_user))
- note.create_new_cross_references!
+ note.create_new_cross_references!(current_user)
note.reset_events_cache
note
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index a6b22348650..e4edc55bf69 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -13,14 +13,14 @@ class NotificationService
# even if user disabled notifications
def new_key(key)
if key.user
- mailer.new_ssh_key_email(key.id)
+ mailer.new_ssh_key_email(key.id).deliver_later
end
end
# Always notify user about email added to profile
def new_email(email)
if email.user
- mailer.new_email_email(email.id)
+ mailer.new_email_email(email.id).deliver_later
end
end
@@ -79,17 +79,27 @@ class NotificationService
end
def merge_mr(merge_request, current_user)
- close_resource_email(merge_request, merge_request.target_project, current_user, 'merged_merge_request_email')
+ close_resource_email(
+ merge_request,
+ merge_request.target_project,
+ current_user,
+ 'merged_merge_request_email'
+ )
end
def reopen_mr(merge_request, current_user)
- reopen_resource_email(merge_request, merge_request.target_project, current_user, 'merge_request_status_email', 'reopened')
+ reopen_resource_email(
+ merge_request,
+ merge_request.target_project,
+ current_user, 'merge_request_status_email',
+ 'reopened'
+ )
end
# Notify new user with email after creation
def new_user(user, token = nil)
# Don't email omniauth created users
- mailer.new_user_email(user.id, token) unless user.identities.any?
+ mailer.new_user_email(user.id, token).deliver_later unless user.identities.any?
end
# Notify users on new note in system
@@ -102,6 +112,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
target = note.noteable
@@ -113,7 +124,7 @@ class NotificationService
end
# Add all users participating in the thread (author, assignee, comment authors)
- participants =
+ participants =
if target.respond_to?(:participants)
target.participants(note.author)
else
@@ -134,53 +145,63 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, note.noteable)
recipients.delete(note.author)
+ recipients = recipients.uniq
# 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)
+ mailer.send(notify_method, recipient.id, note.id).deliver_later
end
end
def invite_project_member(project_member, token)
- mailer.project_member_invited_email(project_member.id, token)
+ mailer.project_member_invited_email(project_member.id, token).deliver_later
end
def accept_project_invite(project_member)
- mailer.project_invite_accepted_email(project_member.id)
+ mailer.project_invite_accepted_email(project_member.id).deliver_later
end
def decline_project_invite(project_member)
- mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id)
+ mailer.project_invite_declined_email(
+ project_member.project.id,
+ project_member.invite_email,
+ project_member.access_level,
+ project_member.created_by_id
+ ).deliver_later
end
def new_project_member(project_member)
- mailer.project_access_granted_email(project_member.id)
+ mailer.project_access_granted_email(project_member.id).deliver_later
end
def update_project_member(project_member)
- mailer.project_access_granted_email(project_member.id)
+ mailer.project_access_granted_email(project_member.id).deliver_later
end
def invite_group_member(group_member, token)
- mailer.group_member_invited_email(group_member.id, token)
+ mailer.group_member_invited_email(group_member.id, token).deliver_later
end
def accept_group_invite(group_member)
- mailer.group_invite_accepted_email(group_member.id)
+ mailer.group_invite_accepted_email(group_member.id).deliver_later
end
def decline_group_invite(group_member)
- mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id)
+ mailer.group_invite_declined_email(
+ group_member.group.id,
+ group_member.invite_email,
+ group_member.access_level,
+ group_member.created_by_id
+ ).deliver_later
end
def new_group_member(group_member)
- mailer.group_access_granted_email(group_member.id)
+ mailer.group_access_granted_email(group_member.id).deliver_later
end
def update_group_member(group_member)
- mailer.group_access_granted_email(group_member.id)
+ mailer.group_access_granted_email(group_member.id).deliver_later
end
def project_was_moved(project, old_path_with_namespace)
@@ -188,7 +209,11 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients.each do |recipient|
- mailer.project_was_moved_email(project.id, recipient.id, old_path_with_namespace)
+ mailer.project_was_moved_email(
+ project.id,
+ recipient.id,
+ old_path_with_namespace
+ ).deliver_later
end
end
@@ -276,35 +301,25 @@ class NotificationService
# Remove users with disabled notifications from array
# Also remove duplications and nil recipients
def reject_muted_users(users, project = nil)
- users = users.to_a.compact.uniq
- users = users.reject(&:blocked?)
-
- users.reject do |user|
- next user.notification.disabled? unless project
-
- member = project.project_members.find_by(user_id: user.id)
-
- if !member && project.group
- member = project.group.group_members.find_by(user_id: user.id)
- end
-
- # reject users who globally disabled notification and has no membership
- next user.notification.disabled? unless member
-
- # reject users who disabled notification in project
- next true if member.notification.disabled?
-
- # reject users who have N_GLOBAL in project and disabled in global settings
- member.notification.global? && user.notification.disabled?
- end
+ reject_users(users, :disabled?, project)
end
# Remove users with notification level 'Mentioned'
def reject_mention_users(users, project = nil)
+ reject_users(users, :mention?, project)
+ end
+
+ # Reject users which method_name from notification object returns true.
+ #
+ # Example:
+ # reject_users(users, :watch?, project)
+ #
+ def reject_users(users, method_name, project = nil)
users = users.to_a.compact.uniq
+ users = users.reject(&:blocked?)
users.reject do |user|
- next user.notification.mention? unless project
+ next user.notification.send(method_name) unless project
member = project.project_members.find_by(user_id: user.id)
@@ -313,19 +328,19 @@ class NotificationService
end
# reject users who globally set mention notification and has no membership
- next user.notification.mention? unless member
+ next user.notification.send(method_name) unless member
# reject users who set mention notification in project
- next true if member.notification.mention?
+ next true if member.notification.send(method_name)
# reject users who have N_MENTION in project and disabled in global settings
- member.notification.global? && user.notification.mention?
+ member.notification.global? && user.notification.send(method_name)
end
end
def reject_unsubscribed_users(recipients, target)
return recipients unless target.respond_to? :subscriptions
-
+
recipients.reject do |user|
subscription = target.subscriptions.find_by_user_id(user.id)
subscription && !subscription.subscribed
@@ -343,12 +358,12 @@ class NotificationService
recipients
end
end
-
+
def new_resource_email(target, project, method)
recipients = build_recipients(target, project, target.author)
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id)
+ mailer.send(method, recipient.id, target.id).deliver_later
end
end
@@ -356,16 +371,24 @@ class NotificationService
recipients = build_recipients(target, project, current_user)
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id, current_user.id)
+ mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
end
end
def reassign_resource_email(target, project, current_user, method)
- assignee_id_was = previous_record(target, "assignee_id")
- recipients = build_recipients(target, project, current_user)
+ previous_assignee_id = previous_record(target, "assignee_id")
+ previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+
+ recipients = build_recipients(target, project, current_user, [previous_assignee])
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id, assignee_id_was, current_user.id)
+ mailer.send(
+ method,
+ recipient.id,
+ target.id,
+ previous_assignee_id,
+ current_user.id
+ ).deliver_later
end
end
@@ -373,13 +396,15 @@ class NotificationService
recipients = build_recipients(target, project, current_user)
recipients.each do |recipient|
- mailer.send(method, recipient.id, target.id, status, current_user.id)
+ mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
end
end
- def build_recipients(target, project, current_user)
+ def build_recipients(target, project, current_user, extra_recipients = nil)
recipients = target.participants(current_user)
+ recipients = recipients.concat(extra_recipients).compact.uniq if extra_recipients
+
recipients = add_project_watchers(recipients, project)
recipients = reject_mention_users(recipients, project)
recipients = reject_muted_users(recipients, project)
@@ -388,12 +413,13 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
+ recipients = recipients.uniq
recipients
end
def mailer
- Notify.delay
+ Notify
end
def previous_record(object, attribute)
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index faf1ee008e7..a6820183bee 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -55,15 +55,19 @@ module Projects
@project.save
if @project.persisted? && !@project.import?
- raise 'Failed to create repository' unless @project.create_repository
+ unless @project.create_repository
+ raise 'Failed to create repository'
+ end
end
end
after_create_actions if @project.persisted?
@project
- rescue
- @project.errors.add(:base, "Can't save project. Please try again later")
+ rescue => e
+ message = "Unable to save project: #{e.message}"
+ Rails.logger.error(message)
+ @project.errors.add(:base, message) if @project
@project
end
@@ -94,11 +98,7 @@ module Projects
@project.team << [current_user, :master, current_user]
end
- @project.update_column(:last_activity_at, @project.created_at)
-
- if @project.import?
- @project.import_start
- end
+ @project.import_start if @project.import?
end
end
end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 46374a3909a..0577ae778d5 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -7,6 +7,8 @@ module Projects
description: @project.description,
name: @project.name,
path: @project.path,
+ shared_runners_enabled: @project.shared_runners_enabled,
+ builds_enabled: @project.builds_enabled,
namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
}
@@ -15,19 +17,6 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
-
- if new_project.persisted?
- if @project.gitlab_ci?
- new_project.enable_ci
-
- settings = @project.gitlab_ci_project.attributes.select do |attr_name, value|
- ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name
- end
-
- new_project.gitlab_ci_project.update(settings)
- end
- end
-
new_project
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 64ea6dd42eb..2e734654466 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -55,6 +55,9 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ project.old_path_with_namespace = old_path
+
+ SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 69bdd045ddf..895e089bea3 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -3,12 +3,16 @@ 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
+ if new_visibility
+ if new_visibility.to_i != project.visibility_level
+ unless can?(current_user, :change_visibility_level, project) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ deny_visibility_level(project, new_visibility)
+ return project
+ end
end
+
+ return false unless visibility_level_allowed?(new_visibility)
end
new_branch = params[:default_branch]
@@ -23,5 +27,19 @@ module Projects
end
end
end
+
+ private
+
+ def visibility_level_allowed?(level)
+ return true if project.visibility_level_allowed?(level)
+
+ level_name = Gitlab::VisibilityLevel.level_name(level)
+ project.errors.add(
+ :visibility_level,
+ "#{level_name} could not be set as visibility level of this project - parent project settings are more restrictive"
+ )
+
+ false
+ end
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 9a5fe4af9dd..6dc854ec33d 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -18,7 +18,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema
+ created_at: model.created_at.xmlschema,
+ updated_at: model.updated_at.xmlschema
}
case model
@@ -33,17 +34,15 @@ class SystemHooksService
)
end
when Project
- owner = model.owner
+ data.merge!(project_data(model))
- data.merge!({
- name: model.name,
- path: model.path,
- path_with_namespace: model.path_with_namespace,
- project_id: model.id,
- owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : "",
- project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
- })
+ if event == :rename || event == :transfer
+ data.merge!({
+ old_path_with_namespace: model.old_path_with_namespace
+ })
+ end
+
+ data
when User
data.merge!({
name: model.name,
@@ -51,16 +50,7 @@ class SystemHooksService
user_id: model.id
})
when ProjectMember
- data.merge!({
- project_name: model.project.name,
- project_path: model.project.path,
- project_path_with_namespace: model.project.path_with_namespace,
- project_id: model.project.id,
- user_name: model.user.name,
- user_email: model.user.email,
- access_level: model.human_access,
- project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
- })
+ data.merge!(project_member_data(model))
when Group
owner = model.owner
@@ -72,15 +62,7 @@ class SystemHooksService
owner_email: owner.respond_to?(:email) ? owner.email : nil,
)
when GroupMember
- data.merge!(
- group_name: model.group.name,
- group_path: model.group.path,
- group_id: model.group.id,
- user_name: model.user.name,
- user_email: model.user.email,
- user_id: model.user.id,
- group_access: model.human_access,
- )
+ data.merge!(group_member_data(model))
end
end
@@ -96,4 +78,43 @@ class SystemHooksService
"#{model.class.name.downcase}_#{event.to_s}"
end
end
+
+ def project_data(model)
+ owner = model.owner
+
+ {
+ name: model.name,
+ path: model.path,
+ path_with_namespace: model.path_with_namespace,
+ project_id: model.id,
+ owner_name: owner.name,
+ owner_email: owner.respond_to?(:email) ? owner.email : "",
+ project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
+ }
+ end
+
+ def project_member_data(model)
+ {
+ project_name: model.project.name,
+ project_path: model.project.path,
+ project_path_with_namespace: model.project.path_with_namespace,
+ project_id: model.project.id,
+ user_name: model.user.name,
+ user_email: model.user.email,
+ access_level: model.human_access,
+ project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
+ }
+ end
+
+ def group_member_data(model)
+ {
+ group_name: model.group.name,
+ group_path: model.group.path,
+ group_id: model.group.id,
+ user_name: model.user.name,
+ user_email: model.user.email,
+ user_id: model.user.id,
+ group_access: model.human_access,
+ }
+ end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 37f454cfc3f..98a71cbf1ad 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -125,7 +125,21 @@ 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}" if source
+ body += " by #{source.gfm_reference(project)}" if source
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when 'merge when build succeeds' is executed
+ def self.merge_when_build_succeeds(noteable, project, author, last_commit)
+ body = "Enabled an automatic merge when the build for #{last_commit.to_reference(project)} succeeds"
+
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
+
+ # Called when 'merge when build succeeds' is canceled
+ def self.cancel_merge_when_build_succeeds(noteable, project, author)
+ body = "Canceled the automatic merge"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -227,9 +241,14 @@ class SystemNoteService
note_options.merge!(noteable: noteable)
end
- create_note(note_options)
+ if noteable.is_a?(ExternalIssue)
+ noteable.project.issues_tracker.create_cross_reference_note(noteable, mentioner, author)
+ else
+ create_note(note_options)
+ end
end
+
def self.cross_reference?(note_text)
note_text.start_with?(cross_reference_note_prefix)
end
@@ -245,7 +264,7 @@ class SystemNoteService
#
# Returns Boolean
def self.cross_reference_disallowed?(noteable, mentioner)
- return true if noteable.is_a?(ExternalIssue)
+ return true if noteable.is_a?(ExternalIssue) && !noteable.project.jira_tracker_active?
return false unless mentioner.is_a?(MergeRequest)
return false unless noteable.is_a?(Commit)
@@ -327,7 +346,7 @@ class SystemNoteService
commit_ids = if count == 1
existing_commits.first.short_id
else
- if oldrev
+ if oldrev && !Gitlab::Git.blank_ref?(oldrev)
"#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
"#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
@@ -341,4 +360,22 @@ class SystemNoteService
"* #{commit_ids} - #{commits_text} from branch `#{branch}`\n"
end
+
+ # Called when the status of a Task has changed
+ #
+ # noteable - Noteable object.
+ # project - Project owning noteable
+ # author - User performing the change
+ # new_task - TaskList::Item object.
+ #
+ # Example Note text:
+ #
+ # "Soandso marked the task Whatever as completed."
+ #
+ # Returns the created Note object
+ def self.change_task_status(noteable, project, author, new_task)
+ status_label = new_task.complete? ? Taskable::COMPLETED : Taskable::INCOMPLETE
+ body = "Marked the task **#{new_task.source}** as #{status_label}"
+ create_note(noteable: noteable, project: project, author: author, note: body)
+ end
end
diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb
new file mode 100644
index 00000000000..25eb13ef09a
--- /dev/null
+++ b/app/services/update_release_service.rb
@@ -0,0 +1,29 @@
+require_relative 'base_service'
+
+class UpdateReleaseService < BaseService
+ def execute(tag_name, release_description)
+
+ repository = project.repository
+ existing_tag = repository.find_tag(tag_name)
+
+ if existing_tag
+ release = project.releases.find_by(tag: tag_name)
+
+ if release
+ release.update_attributes(description: release_description)
+
+ success(release)
+ else
+ error('Release does not exist', 404)
+ end
+ else
+ error('Tag does not exist', 404)
+ end
+ end
+
+ def success(release)
+ out = super()
+ out[:release] = release
+ out
+ end
+end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
new file mode 100644
index 00000000000..1b0ae6c0056
--- /dev/null
+++ b/app/uploaders/artifact_uploader.rb
@@ -0,0 +1,46 @@
+# encoding: utf-8
+class ArtifactUploader < CarrierWave::Uploader::Base
+ storage :file
+
+ attr_accessor :build, :field
+
+ def self.artifacts_path
+ Gitlab.config.artifacts.path
+ end
+
+ def self.artifacts_upload_path
+ File.join(self.artifacts_path, 'tmp/uploads/')
+ end
+
+ def self.artifacts_cache_path
+ File.join(self.artifacts_path, 'tmp/cache/')
+ end
+
+ def initialize(build, field)
+ @build, @field = build, field
+ end
+
+ def store_dir
+ File.join(self.class.artifacts_path, @build.artifacts_path)
+ end
+
+ def cache_dir
+ File.join(self.class.artifacts_cache_path, @build.artifacts_path)
+ end
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+
+ def exists?
+ file.try(:exists?)
+ end
+
+ def move_to_cache
+ true
+ end
+
+ def move_to_store
+ true
+ end
+end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index a9691bee46e..a65a896e41e 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,26 +1,11 @@
# encoding: utf-8
class AttachmentUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
storage :file
def store_dir
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
-
- def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
- end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 7cad044555b..6135c3ad96f 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,6 +1,8 @@
# encoding: utf-8
class AvatarUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
storage :file
after :store, :reset_events_cache
@@ -9,23 +11,6 @@ class AvatarUploader < CarrierWave::Uploader::Base
"uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
end
- def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
- end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
-
def reset_events_cache(file)
model.reset_events_cache if model.is_a?(User)
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index e8211585834..ac920119a85 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,5 +1,7 @@
# encoding: utf-8
class FileUploader < CarrierWave::Uploader::Base
+ include UploaderHelper
+
storage :file
attr_accessor :project, :secret
@@ -28,21 +30,4 @@ class FileUploader < CarrierWave::Uploader::Base
def secure_url
File.join("/uploads", @secret, file.filename)
end
-
- def file_storage?
- self.class.storage == CarrierWave::Storage::File
- end
-
- def image?
- img_ext = %w(png jpg jpeg gif bmp tiff)
- if file.respond_to?(:extension)
- img_ext.include?(file.extension.downcase)
- else
- # Not all CarrierWave storages respond to :extension
- ext = file.path.split('.').last.downcase
- img_ext.include?(ext)
- end
- rescue
- false
- end
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
new file mode 100644
index 00000000000..28085b31083
--- /dev/null
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+
+class LfsObjectUploader < CarrierWave::Uploader::Base
+ storage :file
+
+ def store_dir
+ "#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}"
+ end
+
+ def cache_dir
+ "#{Gitlab.config.lfs.storage_path}/tmp/cache"
+ end
+
+ def move_to_cache
+ true
+ end
+
+ def move_to_store
+ true
+ end
+
+ def exists?
+ file.try(:exists?)
+ end
+
+ def filename
+ model.oid[4..-1]
+ end
+end
diff --git a/app/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb
new file mode 100644
index 00000000000..5ef440f3367
--- /dev/null
+++ b/app/uploaders/uploader_helper.rb
@@ -0,0 +1,19 @@
+# Extra methods for uploader
+module UploaderHelper
+ def image?
+ img_ext = %w(png jpg jpeg gif bmp tiff)
+ if file.respond_to?(:extension)
+ img_ext.include?(file.extension.downcase)
+ else
+ # Not all CarrierWave storages respond to :extension
+ ext = file.path.split('.').last.downcase
+ img_ext.include?(ext)
+ end
+ rescue
+ false
+ end
+
+ def file_storage?
+ self.class.storage == CarrierWave::Storage::File
+ end
+end
diff --git a/app/validators/color_validator.rb b/app/validators/color_validator.rb
new file mode 100644
index 00000000000..571d0007aa2
--- /dev/null
+++ b/app/validators/color_validator.rb
@@ -0,0 +1,20 @@
+# ColorValidator
+#
+# Custom validator for web color codes. It requires the leading hash symbol and
+# will accept RGB triplet or hexadecimal formats.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :background_color, allow_blank: true, color: true
+# end
+#
+class ColorValidator < ActiveModel::EachValidator
+ PATTERN = /\A\#[0-9A-Fa-f]{3}{1,2}+\Z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value =~ PATTERN
+ record.errors.add(attribute, "must be a valid color code")
+ end
+ end
+end
diff --git a/lib/email_validator.rb b/app/validators/email_validator.rb
index f509f0a5843..b35af100803 100644
--- a/lib/email_validator.rb
+++ b/app/validators/email_validator.rb
@@ -1,3 +1,5 @@
+# EmailValidator
+#
# Based on https://github.com/balexand/email_validator
#
# Extended to use only strict mode with following allowed characters:
@@ -6,15 +8,10 @@
# See http://www.remote.org/jochen/mail/info/chars.html
#
class EmailValidator < ActiveModel::EachValidator
- @@default_options = {}
-
- def self.default_options
- @@default_options
- end
+ PATTERN = /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i.freeze
def validate_each(record, attribute, value)
- options = @@default_options.merge(self.options)
- unless value =~ /\A\s*([-a-z0-9+._']{1,64})@((?:[-a-z0-9]+\.)+[a-z]{2,})\s*\z/i
+ unless value =~ PATTERN
record.errors.add(attribute, options[:message] || :invalid)
end
end
diff --git a/app/validators/line_code_validator.rb b/app/validators/line_code_validator.rb
new file mode 100644
index 00000000000..ed29e5aeb67
--- /dev/null
+++ b/app/validators/line_code_validator.rb
@@ -0,0 +1,12 @@
+# LineCodeValidator
+#
+# Custom validator for GitLab line codes.
+class LineCodeValidator < ActiveModel::EachValidator
+ PATTERN = /\A[a-z0-9]+_\d+_\d+\z/.freeze
+
+ def validate_each(record, attribute, value)
+ unless value =~ PATTERN
+ record.errors.add(attribute, "must be a valid line code")
+ end
+ end
+end
diff --git a/app/validators/namespace_name_validator.rb b/app/validators/namespace_name_validator.rb
new file mode 100644
index 00000000000..2e51af2982d
--- /dev/null
+++ b/app/validators/namespace_name_validator.rb
@@ -0,0 +1,10 @@
+# NamespaceNameValidator
+#
+# Custom validator for GitLab namespace name strings.
+class NamespaceNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value =~ Gitlab::Regex.namespace_name_regex
+ record.errors.add(attribute, Gitlab::Regex.namespace_name_regex_message)
+ end
+ end
+end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
new file mode 100644
index 00000000000..10e35ce665a
--- /dev/null
+++ b/app/validators/namespace_validator.rb
@@ -0,0 +1,50 @@
+# NamespaceValidator
+#
+# Custom validator for GitLab namespace values.
+#
+# Values are checked for formatting and exclusion from a list of reserved path
+# names.
+class NamespaceValidator < ActiveModel::EachValidator
+ RESERVED = %w(
+ admin
+ all
+ assets
+ ci
+ dashboard
+ files
+ groups
+ help
+ hooks
+ issues
+ merge_requests
+ notes
+ profile
+ projects
+ public
+ repository
+ s
+ search
+ services
+ snippets
+ teams
+ u
+ unsubscribes
+ users
+ ).freeze
+
+ def validate_each(record, attribute, value)
+ unless value =~ Gitlab::Regex.namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
+ end
+
+ if reserved?(value)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+
+ private
+
+ def reserved?(value)
+ RESERVED.include?(value)
+ end
+end
diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb
new file mode 100644
index 00000000000..2848b9cd33d
--- /dev/null
+++ b/app/validators/url_validator.rb
@@ -0,0 +1,36 @@
+# UrlValidator
+#
+# Custom validator for URLs.
+#
+# By default, only URLs for the HTTP(S) protocols will be considered valid.
+# Provide a `:protocols` option to configure accepted protocols.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, url: true
+#
+# validates :ftp_url, url: { protocols: %w(ftp) }
+#
+# validates :git_url, url: { protocols: %w(http https ssh git) }
+# end
+#
+class UrlValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless valid_url?(value)
+ record.errors.add(attribute, "must be a valid URL")
+ end
+ end
+
+ private
+
+ def default_options
+ @default_options ||= { protocols: %w(http https) }
+ end
+
+ def valid_url?(value)
+ options = default_options.merge(self.options)
+
+ value =~ /\A#{URI.regexp(options[:protocols])}\z/
+ end
+end
diff --git a/app/views/abuse_report_mailer/notify.html.haml b/app/views/abuse_report_mailer/notify.html.haml
index 619533e09a7..2741eb44357 100644
--- a/app/views/abuse_report_mailer/notify.html.haml
+++ b/app/views/abuse_report_mailer/notify.html.haml
@@ -8,4 +8,4 @@
= @abuse_report.message
%p
- = link_to "View details", abuse_reports_url
+ = link_to "View details", admin_abuse_reports_url
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index cffd7684008..3e5cdd2ce4a 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -2,7 +2,7 @@
%h3.page-title Report abuse
%p Please use this form to report users who create spam issues, comments or behave inappropriately.
%hr
-= form_for @abuse_report, html: { class: 'form-horizontal'} do |f|
+= form_for @abuse_report, html: { class: 'form-horizontal js-requires-input'} do |f|
= f.hidden_field :user_id
- if @abuse_report.errors.any?
.alert.alert-danger
@@ -16,7 +16,7 @@
.form-group
= f.label :message, class: 'control-label'
.col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
+ = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true
.help-block
Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment.
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index d3afc658cd6..cf50a376e11 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -2,19 +2,21 @@
- user = abuse_report.user
%tr
%td
- - if reporter
- = link_to reporter.name, reporter
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
- else
(removed)
%td
- = abuse_report.created_at.to_s(:short)
- %td
- = abuse_report.message
- %td
- - if user
- = link_to user.name, user
+ - if reporter
+ = link_to reporter.name, [:admin, reporter]
- else
(removed)
+ .light.small
+ = time_ago_with_tooltip(abuse_report.created_at)
+ %td
+ = markdown(abuse_report.message.squish!, pipeline: :single_line)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 40a5fe4628b..bc4a9cedb2c 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -6,10 +6,9 @@
%table.table
%thead
%tr
+ %th User
%th Reported by
- %th Reported at
%th Message
- %th User
%th Primary action
%th
= render @abuse_reports
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 7a78526e09a..89b38a0dad0 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -14,11 +14,11 @@
.form-group.project-visibility-level-holder
= f.label :default_project_visibility, class: 'control-label col-sm-2'
.col-sm-10
- = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project')
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project)
.form-group.project-visibility-level-holder
= f.label :default_snippet_visibility, class: 'control-label col-sm-2'
.col-sm-10
- = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet')
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: PersonalSnippet)
.form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10
@@ -105,6 +105,18 @@
= f.check_box :signin_enabled
Sign-in enabled
.form-group
+ = f.label :two_factor_authentication, 'Two-Factor authentication', class: 'control-label col-sm-2'
+ .col-sm-10
+ .checkbox
+ = f.label :require_two_factor_authentication do
+ = f.check_box :require_two_factor_authentication
+ Require all users to setup Two-Factor authentication
+ .form-group
+ = f.label :two_factor_authentication, 'Two-Factor grace period (hours)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0'
+ .help-block Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication
+ .form-group
= f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
.col-sm-10
= f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
@@ -130,5 +142,96 @@
= f.text_area :help_page_text, class: 'form-control', rows: 4
.help-block Markdown enabled
+ %fieldset
+ %legend Continuous Integration
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :shared_runners_enabled do
+ = f.check_box :shared_runners_enabled
+ Enable shared runners for new projects
+
+ .form-group
+ = f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_artifacts_size, class: 'form-control'
+
+ %fieldset
+ %legend Metrics
+ %p
+ These settings require a restart to take effect.
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :metrics_enabled do
+ = f.check_box :metrics_enabled
+ Enable InfluxDB Metrics
+ .form-group
+ = f.label :metrics_host, 'InfluxDB host', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
+ .form-group
+ = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
+ .help-block
+ The UDP port to use for connecting to InfluxDB. InfluxDB requires that
+ your server configuration specifies a database to store data in when
+ sending messages to this port, without it metrics data will not be
+ saved.
+ .form-group
+ = f.label :metrics_username, 'InfluxDB username', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_username, class: 'form-control'
+ .form-group
+ = f.label :metrics_password, 'InfluxDB password', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :metrics_password, class: 'form-control'
+ .form-group
+ = f.label :metrics_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_pool_size, class: 'form-control'
+ .help-block
+ The amount of InfluxDB connections to open. Connections are opened
+ lazily. Users using multi-threaded application servers should ensure
+ enough connections are available (at minimum the amount of application
+ server threads).
+ .form-group
+ = f.label :metrics_timeout, 'Connection timeout', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_timeout, class: 'form-control'
+ .help-block
+ The amount of seconds after which an InfluxDB connection will time
+ out.
+ .form-group
+ = f.label :metrics_method_call_threshold, 'Method Call Threshold (ms)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_method_call_threshold, class: 'form-control'
+ .help-block
+ A method call is only tracked when it takes longer to complete than
+ the given amount of milliseconds.
+
+ %fieldset
+ %legend Spam and Anti-bot Protection
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :recaptcha_enabled do
+ = f.check_box :recaptcha_enabled
+ Enable reCAPTCHA
+ %span.help-block#recaptcha_help_block Helps preventing bots from creating accounts
+
+ .form-group
+ = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_site_key, class: 'form-control'
+ .help-block
+ Generate site and private keys here:
+ %a{ href: 'http://www.google.com/recaptcha', target: 'blank'} http://www.google.com/recaptcha
+ .form-group
+ = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :recaptcha_private_key, class: 'form-control'
+
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
new file mode 100644
index 00000000000..6936e614346
--- /dev/null
+++ b/app/views/admin/builds/_build.html.haml
@@ -0,0 +1,73 @@
+- project = build.project
+%tr.build
+ %td.status
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ - if build.target_url
+ = link_to build.target_url do
+ %strong Build ##{build.id}
+ - else
+ %strong Build ##{build.id}
+
+ - if build.show_warning?
+ %i.fa.fa-warning.text-warning
+
+ %td
+ - if project
+ = link_to project.name_with_namespace, admin_namespace_project_path(project.namespace, project), class: "monospace"
+
+ %td
+ = link_to build.short_sha, namespace_project_commit_path(project.namespace, project, build.sha), class: "monospace"
+
+ %td
+ - if build.ref
+ = link_to build.ref, namespace_project_commits_path(project.namespace, project, build.ref)
+ - else
+ .light none
+
+ %td
+ - if build.try(:runner)
+ = runner_link(build.runner)
+ - else
+ .light none
+
+ %td
+ #{build.stage} / #{build.name}
+
+ .pull-right
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+
+ %td.duration
+ - if build.duration
+ #{duration_in_words(build.finished_at, build.started_at)}
+
+ %td.timestamp
+ - if build.finished_at
+ %span #{time_ago_with_tooltip(build.finished_at)}
+
+ - if defined?(coverage) && coverage
+ %td.coverage
+ - if build.try(:coverage)
+ #{build.coverage}%
+
+ %td
+ .pull-right
+ - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
+ = link_to build.download_url, title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if current_user && can?(current_user, :manage_builds, build.project)
+ - if build.active?
+ - if build.cancel_url
+ = link_to build.cancel_url, method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
+ - elsif defined?(allow_retry) && allow_retry && build.retry_url
+ = link_to build.retry_url, method: :post, title: 'Retry' do
+ %i.fa.fa-repeat
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
new file mode 100644
index 00000000000..ddd4e1481eb
--- /dev/null
+++ b/app/views/admin/builds/index.html.haml
@@ -0,0 +1,50 @@
+.project-issuable-filter
+ .controls
+ .pull-left.hidden-xs
+ - if @all_builds.running_or_pending.any?
+ = link_to 'Cancel all', cancel_all_admin_builds_path, data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+
+ %ul.center-top-menu
+ %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))
+
+.gray-content-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 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/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 8657d2c71fe..cc389c3ae08 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -6,35 +6,35 @@
%p
Forks
%span.light.pull-right
- = ForkedProjectLink.count
+ = number_with_delimiter(ForkedProjectLink.count)
%p
Issues
%span.light.pull-right
- = Issue.count
+ = number_with_delimiter(Issue.count)
%p
Merge Requests
%span.light.pull-right
- = MergeRequest.count
+ = number_with_delimiter(MergeRequest.count)
%p
Notes
%span.light.pull-right
- = Note.count
+ = number_with_delimiter(Note.count)
%p
Snippets
%span.light.pull-right
- = Snippet.count
+ = number_with_delimiter(Snippet.count)
%p
SSH Keys
%span.light.pull-right
- = Key.count
+ = number_with_delimiter(Key.count)
%p
Milestones
%span.light.pull-right
- = Milestone.count
+ = number_with_delimiter(Milestone.count)
%p
Active Users
%span.light.pull-right
- = User.active.count
+ = number_with_delimiter(User.active.count)
.col-md-4
%h4
Features
@@ -80,6 +80,10 @@
%span.pull-right
= API::API::version
%p
+ Git
+ %span.pull-right
+ = Gitlab::Git.version
+ %p
Ruby
%span.pull-right
#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}
@@ -95,7 +99,7 @@
%h4 Projects
.data
= link_to admin_namespaces_projects_path do
- %h1= Project.count
+ %h1= number_with_delimiter(Project.count)
%hr
= link_to('New Project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -103,7 +107,7 @@
%h4 Users
.data
= link_to admin_users_path do
- %h1= User.count
+ %h1= number_with_delimiter(User.count)
%hr
= link_to 'New User', new_admin_user_path, class: "btn btn-new"
.col-sm-4
@@ -111,7 +115,7 @@
%h4 Groups
.data
= link_to admin_groups_path do
- %h1= Group.count
+ %h1= number_with_delimiter(Group.count)
%hr
= link_to 'New Group', new_admin_group_path, class: "btn btn-new"
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 5ce7cdf2f8d..3940210e19b 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Groups"
%h3.page-title
- Groups (#{@groups.total_count})
+ Groups (#{number_with_delimiter(@groups.total_count)})
= link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
%p.light
diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml
index 8358a14445b..741d111fb7d 100644
--- a/app/views/admin/identities/index.html.haml
+++ b/app/views/admin/identities/index.html.haml
@@ -1,6 +1,7 @@
- page_title "Identities", @user.name, "Users"
= render 'admin/users/head'
+= link_to 'New Identity', new_admin_user_identity_path, class: 'pull-right btn btn-new'
- if @identities.present?
.table-holder
%table.table
diff --git a/app/views/admin/identities/new.html.haml b/app/views/admin/identities/new.html.haml
new file mode 100644
index 00000000000..e30bf0ef0ee
--- /dev/null
+++ b/app/views/admin/identities/new.html.haml
@@ -0,0 +1,4 @@
+- page_title "New Identity"
+%h3.page-title New identity
+%hr
+= render 'form'
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index ad58a3837f6..eaa94ed9e36 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -12,7 +12,7 @@
.col-sm-10
= f.text_field :title, class: "form-control", required: true
.form-group
- = f.label :color, "Background Color", class: 'control-label'
+ = f.label :color, "Background color", class: 'control-label'
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
@@ -31,5 +31,5 @@
= f.submit 'Save', class: 'btn btn-save js-save-button'
= link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
-:coffeescript
- new Labels
+:javascript
+ new Labels();
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index 596e06243dd..e3ccbf6c3a8 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -2,4 +2,4 @@
= render_colored_label(label)
.pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
- = link_to 'Remove', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+ = link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
index 45c62a76259..309aedceded 100644
--- a/app/views/admin/labels/edit.html.haml
+++ b/app/views/admin/labels/edit.html.haml
@@ -1,9 +1,5 @@
- page_title "Edit", @label.name, "Labels"
-%h3
- Edit label
- %span.light #{@label.name}
-.back-link
- = link_to admin_labels_path do
- &larr; To labels list
+%h3.page-title
+ Edit Label
%hr
= render 'form'
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
index 8d298ad20f7..0135ad0723d 100644
--- a/app/views/admin/labels/new.html.haml
+++ b/app/views/admin/labels/new.html.haml
@@ -1,7 +1,5 @@
- page_title "New Label"
-%h3 New label
-.back-link
- = link_to admin_labels_path do
- &larr; To labels list
+%h3.page-title
+ New Label
%hr
= render 'form'
diff --git a/app/views/ci/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 701782d26bb..6745e58deca 100644
--- a/app/views/ci/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -8,14 +8,14 @@
%span.label.label-danger paused
%td
- = link_to ci_admin_runner_path(runner) do
+ = link_to admin_runner_path(runner) do
= runner.short_sha
%td
.runner-description
= runner.description
%span (#{link_to 'edit', '#', class: 'edit-runner-link'})
.runner-description-form.hide
- = form_for [:ci, :admin, runner], remote: true, html: { class: 'form-inline' } do |f|
+ = form_for [:admin, runner], remote: true, html: { class: 'form-inline' } do |f|
.form-group
= f.text_field :description, class: 'form-control'
= f.submit 'Save', class: 'btn'
@@ -38,11 +38,11 @@
Never
%td
.pull-right
- = link_to 'Edit', ci_admin_runner_path(runner), class: 'btn btn-sm'
+ = link_to 'Edit', admin_runner_path(runner), class: 'btn btn-sm'
&nbsp;
- if runner.active?
- = link_to 'Pause', [:pause, :ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm'
+ = link_to 'Pause', [:pause, :admin, runner], data: { confirm: "Are you sure?" }, method: :get, class: 'btn btn-danger btn-sm'
- else
- = link_to 'Resume', [:resume, :ci, :admin, runner], method: :get, class: 'btn btn-success btn-sm'
- = link_to 'Remove', [:ci, :admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to 'Resume', [:resume, :admin, runner], method: :get, class: 'btn btn-success btn-sm'
+ = link_to 'Remove', [:admin, runner], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
diff --git a/app/views/ci/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index bb213fbffc4..c407972cd08 100644
--- a/app/views/ci/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,6 +1,20 @@
%p.lead
- %span To register 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.
- %code #{GitlabCi::REGISTRATION_TOKEN}
+ %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}
+
+.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
@@ -21,11 +35,11 @@
\- run builds from assigned projects
%li
%span.label.label-danger paused
- \- runner will not receive any new build
+ \- runner will not receive any new builds
.append-bottom-20.clearfix
.pull-left
- = form_tag ci_admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
+ = 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'
@@ -49,5 +63,5 @@
%th
- @runners.each do |runner|
- = render "ci/admin/runners/runner", runner: runner
+ = render "admin/runners/runner", runner: runner
= paginate @runners
diff --git a/app/views/ci/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 92787b2e6ac..8700b4820cd 100644
--- a/app/views/ci/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -13,16 +13,16 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This runner will process build from ALL UNASSIGNED projects
+ %h4 This runner will process builds from ALL UNASSIGNED projects
%p
If you want runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This runner will process build only from ASSIGNED projects
+ %h4 This runner will process builds only from ASSIGNED projects
%p You can't make this a shared runner.
%hr
-= form_for @runner, url: ci_admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
+= form_for @runner, url: admin_runner_path(@runner), html: { class: 'form-horizontal' } do |f|
.form-group
= label_tag :token, class: 'control-label' do
Token
@@ -37,7 +37,7 @@
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
- = f.text_field :tag_list, class: 'form-control'
+ = 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'
@@ -53,28 +53,24 @@
%th
- @runner.runner_projects.each do |runner_project|
- project = runner_project.project
- %tr.alert-info
- %td
- %strong
- = project.name
- %td
- .pull-right
- = link_to 'Disable', [:ci, :admin, project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
+ - if project
+ %tr.alert-info
+ %td
+ %strong
+ = project.name_with_namespace
+ %td
+ .pull-right
+ = link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
%table.table
%thead
%tr
%th Project
%th
- .pull-right
- = link_to 'Assign to all', assign_all_ci_admin_runner_path(@runner),
- class: 'btn btn-sm assign-all-runner',
- title: 'Assign runner to all projects',
- method: :put
%tr
%td
- = form_tag ci_admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
+ = form_tag admin_runner_path(@runner), id: 'runner-projects-search', class: 'form-inline', method: :get do
.form-group
= search_field_tag :search, params[:search], class: 'form-control', spellcheck: false
= submit_tag 'Search', class: 'btn'
@@ -83,41 +79,46 @@
- @projects.each do |project|
%tr
%td
- = project.name
+ = project.name_with_namespace
%td
.pull-right
- = form_for [:ci, :admin, project, project.runner_projects.new] do |f|
+ = form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: @runner.id
= f.submit 'Enable', class: 'btn btn-xs'
= paginate @projects
.col-md-6
%h4 Recent builds served by this runner
- %table.builds.runner-builds
+ %table.table.builds.runner-builds
%thead
%tr
- %th Build ID
+ %th Build
%th Status
%th Project
%th Commit
%th Finished at
- @builds.each do |build|
+ - project = build.project
%tr.build
%td.id
- - gl_project = build.project.gl_project
- = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
- = build.id
+ - if project
+ = link_to namespace_project_build_path(project.namespace, project, build) do
+ %strong ##{build.id}
+ - else
+ %strong ##{build.id}
%td.status
= ci_status_with_icon(build.status)
%td.status
- = build.project.name
+ - if project
+ = project.name_with_namespace
%td.build-link
- = link_to ci_status_path(build.commit) do
- %strong #{build.commit.short_sha}
+ - if project
+ = link_to ci_status_path(build.commit) do
+ %strong #{build.commit.short_sha}
%td.timestamp
- if build.finished_at
diff --git a/app/views/ci/admin/runners/update.js.haml b/app/views/admin/runners/update.js.haml
index 2b7d3067e20..2b7d3067e20 100644
--- a/app/views/ci/admin/runners/update.js.haml
+++ b/app/views/admin/runners/update.js.haml
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 4245d0f1eda..5e17b018163 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -6,8 +6,8 @@
%span.cred (Admin)
.pull-right
- - unless @user == current_user
- = link_to 'Log in as this user', login_as_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info"
+ - unless @user == current_user || @user.blocked?
+ = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 90d9980c85c..7d11edc79e2 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -16,11 +16,11 @@
- unless user.linkedin.blank?
%li
%span.light LinkedIn:
- %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}"
+ %strong= link_to user.linkedin, "https://www.linkedin.com/in/#{user.linkedin}"
- unless user.twitter.blank?
%li
%span.light Twitter:
- %strong= link_to user.twitter, "http://www.twitter.com/#{user.twitter}"
+ %strong= link_to user.twitter, "https://twitter.com/#{user.twitter}"
- unless user.website_url.blank?
%li
%span.light Website:
diff --git a/app/views/users/_projects.html.haml b/app/views/admin/users/_projects.html.haml
index a126a858ea8..a126a858ea8 100644
--- a/app/views/users/_projects.html.haml
+++ b/app/views/admin/users/_projects.html.haml
diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml
index a8837d74dd9..3b6fd71500d 100644
--- a/app/views/admin/users/edit.html.haml
+++ b/app/views/admin/users/edit.html.haml
@@ -1,8 +1,5 @@
- page_title "Edit", @user.name, "Users"
%h3.page-title
Edit user: #{@user.name}
-.back-link
- = link_to admin_user_path(@user) do
- &larr; Back to user page
%hr
= render 'form'
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index bc08458312c..a92c9c152b9 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -8,27 +8,27 @@
%li{class: "#{'active' unless params[:filter]}"}
= link_to admin_users_path do
Active
- %small.pull-right= User.active.count
+ %small.pull-right= number_with_delimiter(User.active.count)
%li{class: "#{'active' if params[:filter] == "admins"}"}
= link_to admin_users_path(filter: "admins") do
Admins
- %small.pull-right= User.admins.count
+ %small.pull-right= 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.pull-right= User.with_two_factor.count
+ %small.pull-right= 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.pull-right= User.without_two_factor.count
+ %small.pull-right= number_with_delimiter(User.without_two_factor.count)
%li{class: "#{'active' if params[:filter] == "blocked"}"}
= link_to admin_users_path(filter: "blocked") do
Blocked
- %small.pull-right= User.blocked.count
+ %small.pull-right= number_with_delimiter(User.blocked.count)
%li{class: "#{'active' if params[:filter] == "wop"}"}
= link_to admin_users_path(filter: "wop") do
Without projects
- %small.pull-right= User.without_projects.count
+ %small.pull-right= number_with_delimiter(User.without_projects.count)
%hr
= form_tag admin_users_path, method: :get, class: 'form-inline' do
.form-group
@@ -42,7 +42,7 @@
%section.col-md-9
.panel.panel-default
.panel-heading
- Users (#{@users.total_count})
+ Users (#{number_with_delimiter(@users.total_count)})
.panel-head-actions
.dropdown.inline
%a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 0d7a1a25a80..b655b2a15f5 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -14,7 +14,7 @@
.row
.col-md-6
- if @personal_projects.present?
- = render 'users/projects', projects: @personal_projects
+ = render 'admin/users/projects', projects: @personal_projects
- else
.nothing-here-block This user has no personal projects.
diff --git a/app/views/ci/admin/application_settings/_form.html.haml b/app/views/ci/admin/application_settings/_form.html.haml
deleted file mode 100644
index 634c9daa477..00000000000
--- a/app/views/ci/admin/application_settings/_form.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-= form_for @application_setting, url: ci_admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
- - if @application_setting.errors.any?
- #error_explanation
- .alert.alert-danger
- - @application_setting.errors.full_messages.each do |msg|
- %p= msg
-
- %fieldset
- %legend Default Project Settings
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :all_broken_builds do
- = f.check_box :all_broken_builds
- Send emails only on broken builds
- .form-group
- .col-sm-offset-2.col-sm-10
- .checkbox
- = f.label :add_pusher do
- = f.check_box :add_pusher
- Add pusher to recipients list
-
- .form-actions
- = f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/ci/admin/application_settings/show.html.haml b/app/views/ci/admin/application_settings/show.html.haml
deleted file mode 100644
index 7ef0aa89ed6..00000000000
--- a/app/views/ci/admin/application_settings/show.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-%h3.page-title Settings
-%hr
-= render 'form'
diff --git a/app/views/ci/admin/builds/_build.html.haml b/app/views/ci/admin/builds/_build.html.haml
deleted file mode 100644
index 2df58713214..00000000000
--- a/app/views/ci/admin/builds/_build.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-- gl_project = build.project.gl_project
-- if build.commit && build.project
- %tr.build
- %td.build-link
- = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
- %strong #{build.id}
-
- %td.status
- = ci_status_with_icon(build.status)
-
- %td.commit-link
- = link_to ci_status_path(build.commit) do
- %strong #{build.commit.short_sha}
-
- %td.runner
- - if build.runner
- = link_to build.runner.id, ci_admin_runner_path(build.runner)
-
- %td.build-project
- = truncate build.project.name, length: 30
-
- %td.build-message
- %span= truncate(build.commit.git_commit_message, length: 30)
-
- %td.build-branch
- %span= truncate(build.ref, length: 25)
-
- %td.duration
- - if build.duration
- #{duration_in_words(build.finished_at, build.started_at)}
-
- %td.timestamp
- - if build.finished_at
- %span #{time_ago_in_words build.finished_at} ago
diff --git a/app/views/ci/admin/builds/index.html.haml b/app/views/ci/admin/builds/index.html.haml
deleted file mode 100644
index d23119162cc..00000000000
--- a/app/views/ci/admin/builds/index.html.haml
+++ /dev/null
@@ -1,28 +0,0 @@
-%ul.nav.nav-tabs.append-bottom-20
- %li{class: ("active" if @scope.nil?)}
- = link_to 'All builds', ci_admin_builds_path
-
- %li{class: ("active" if @scope == "pending")}
- = link_to "Pending", ci_admin_builds_path(scope: :pending)
-
- %li{class: ("active" if @scope == "running")}
- = link_to "Running", ci_admin_builds_path(scope: :running)
-
-
-%table.builds
- %thead
- %tr
- %th Build
- %th Status
- %th Commit
- %th Runner
- %th Project
- %th Message
- %th Branch
- %th Duration
- %th Finished at
-
- - @builds.each do |build|
- = render "ci/admin/builds/build", build: build
-
-= paginate @builds
diff --git a/app/views/ci/admin/events/index.html.haml b/app/views/ci/admin/events/index.html.haml
deleted file mode 100644
index 5a5b4dc7c35..00000000000
--- a/app/views/ci/admin/events/index.html.haml
+++ /dev/null
@@ -1,18 +0,0 @@
-.table-holder
- %table.table
- %thead
- %tr
- %th User ID
- %th Description
- %th When
- - @events.each do |event|
- %tr
- %td
- = event.user_id
- %td
- = event.description
- %td.light
- = time_ago_in_words event.updated_at
- ago
-
-= paginate @events
diff --git a/app/views/ci/admin/projects/_project.html.haml b/app/views/ci/admin/projects/_project.html.haml
deleted file mode 100644
index a342d6e1cf0..00000000000
--- a/app/views/ci/admin/projects/_project.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-- last_commit = project.commits.last
-%tr
- %td
- = project.id
- %td
- = link_to [:ci, project] do
- %strong= project.name
- %td
- - if last_commit
- = ci_status_with_icon(last_commit.status)
- - if project.last_commit_date
- &middot;
- = time_ago_in_words project.last_commit_date
- ago
- - else
- No builds yet
- %td
- - if project.public
- %i.fa.fa-globe
- Public
- - else
- %i.fa.fa-lock
- Private
- %td
- = project.commits.count
- %td
- = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do
- %i.fa.fa-remove
- Remove
diff --git a/app/views/ci/admin/projects/index.html.haml b/app/views/ci/admin/projects/index.html.haml
deleted file mode 100644
index 0da8547924b..00000000000
--- a/app/views/ci/admin/projects/index.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.table-holder
- %table.table
- %thead
- %tr
- %th ID
- %th Name
- %th Last build
- %th Access
- %th Builds
- %th
-
- - @projects.each do |project|
- = render "ci/admin/projects/project", project: project
-
-= paginate @projects
-
diff --git a/app/views/ci/admin/runner_projects/index.html.haml b/app/views/ci/admin/runner_projects/index.html.haml
deleted file mode 100644
index f049b4f4c4e..00000000000
--- a/app/views/ci/admin/runner_projects/index.html.haml
+++ /dev/null
@@ -1,57 +0,0 @@
-%p.lead
- To register new runner visit #{link_to 'this page ', ci_runners_path}
-
-.row
- .col-md-8
- %h5 Activated:
- %table.table
- %tr
- %th Runner ID
- %th Runner Description
- %th Last build
- %th Builds Stats
- %th Registered
- %th
-
- - @runner_projects.each do |runner_project|
- - runner = runner_project.runner
- - builds = runner.builds.where(project_id: @project.id)
- %tr
- %td
- %span.badge.badge-info= runner.id
- %td
- = runner.display_name
- %td
- - last_build = builds.last
- - if last_build
- = link_to last_build.short_sha, [last_build.project, last_build]
- - else
- unknown
- %td
- %span.badge.badge-success
- #{builds.success.count}
- %span /
- %span.badge.badge-important
- #{builds.failed.count}
- %td
- #{time_ago_in_words(runner_project.created_at)} ago
- %td
- = link_to 'Disable', [:ci, @project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm right'
- .col-md-4
- %h5 Available
- %table.table
- %tr
- %th ID
- %th Token
- %th
-
- - (Ci::Runner.all - @project.runners).each do |runner|
- %tr
- %td
- = runner.id
- %td
- = runner.token
- %td
- = form_for [:ci, @project, @runner_project] do |f|
- = f.hidden_field :runner_id, value: runner.id
- = f.submit 'Add', class: 'btn btn-sm'
diff --git a/app/views/ci/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
index b24a3b826cf..11163813f3e 100644
--- a/app/views/ci/commits/_commit.html.haml
+++ b/app/views/ci/commits/_commit.html.haml
@@ -27,7 +27,6 @@
- if commit.finished_at
%span #{time_ago_in_words commit.finished_at} ago
- - if commit.project.coverage_enabled?
+ - if commit.coverage
%td.coverage
- - if commit.coverage
- #{commit.coverage}%
+ #{commit.coverage}%
diff --git a/app/views/ci/events/index.html.haml b/app/views/ci/events/index.html.haml
deleted file mode 100644
index 9824e85b1af..00000000000
--- a/app/views/ci/events/index.html.haml
+++ /dev/null
@@ -1,20 +0,0 @@
-%h3.page-title Events
-
-.table-holder
- %table.table
- %thead
- %tr
- %th User ID
- %th Description
- %th When
- - @events.each do |event|
- %tr
- %td
- = event.user_id
- %td
- = event.description
- %td.light
- = time_ago_in_words event.updated_at
- ago
-
-= paginate @events
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f45cd05aec0..f7875e68b7e 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -17,7 +17,7 @@
%td #{stage.capitalize} Job - #{build[:name]}
%td
%pre
- = simple_format build[:script]
+ = simple_format build[:commands]
%br
%b Tag list:
@@ -28,6 +28,11 @@
%br
%b Refs except:
= build[:except] && build[:except].join(", ")
+ %br
+ %b When:
+ = build[:when]
+ - if build[:allow_failure]
+ %b Allowed to fail
-else
%p
@@ -36,5 +41,3 @@
%i.fa.fa-remove.incorrect-syntax
%b Error:
= @error
-
-
diff --git a/app/views/ci/lints/create.js.haml b/app/views/ci/lints/create.js.haml
deleted file mode 100644
index a96c0b11b6e..00000000000
--- a/app/views/ci/lints/create.js.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-:plain
- $(".results").html("#{escape_javascript(render "create")}") \ No newline at end of file
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index a9b954771c5..a144c43be47 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,25 +1,17 @@
%h2 Check your .gitlab-ci.yml
%hr
-= form_tag ci_lint_path, method: :post, remote: true do
- .control-group
- = label_tag :content, "Content of .gitlab-ci.yml", class: 'control-label'
- .controls
- = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+.row
+ = form_tag ci_lint_path, method: :post do
+ .form-group
+ = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap'
+ .col-sm-12
+ = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+ .col-sm-12
+ .pull-left.prepend-top-10
+ = submit_tag 'Validate', class: 'btn btn-success submit-yml'
- .control-group.clearfix
- .controls.pull-left.prepend-top-10
- = submit_tag "Validate", class: 'btn btn-success submit-yml'
-
-
-%p.text-center.loading
- %i.fa.fa-refresh.fa-spin
-
-.results.prepend-top-20
-
-:coffeescript
- $(".loading").hide()
- $('form').bind 'ajax:beforeSend', ->
- $(".loading").show()
- $('form').bind 'ajax:complete', ->
- $(".loading").hide()
+.row.prepend-top-20
+ .col-sm-12
+ .results
+ = render partial: 'create' if defined?(@status)
diff --git a/app/views/ci/shared/_guide.html.haml b/app/views/ci/shared/_guide.html.haml
index db2d7f2f4b6..09e7e653521 100644
--- a/app/views/ci/shared/_guide.html.haml
+++ b/app/views/ci/shared/_guide.html.haml
@@ -4,12 +4,10 @@
%ol
%li
Add at least one runner to the project.
- Go to #{link_to 'Runners page', runners_path(@project.gl_project), target: :blank} for instructions.
+ 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}.
+ 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
- Visit #{link_to 'GitLab project settings', @project.gitlab_url + "/services/gitlab_ci/edit", target: :blank}
- and press the "Test settings" button.
- %li
Return to this page and refresh it, it should show a new build.
diff --git a/app/views/ci/user_sessions/new.html.haml b/app/views/ci/user_sessions/new.html.haml
deleted file mode 100644
index 308b217ea78..00000000000
--- a/app/views/ci/user_sessions/new.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-.login-block
- %h2 Login using GitLab account
- %p.light
- Make sure you have account on GitLab server
- = link_to GitlabCi.config.gitlab_server.url, GitlabCi.config.gitlab_server.url, no_turbolink
- %hr
- = link_to "Login with GitLab", auth_ci_user_sessions_path(state: params[:state]), no_turbolink.merge( class: 'btn btn-login btn-success' )
-
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index ed480b8caf8..f4a3e3162bf 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,10 +1,20 @@
-%ul.center-top-menu
- = nav_link(path: ['projects#index', 'root#index']) do
- = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
- Your Projects
- = nav_link(page: starred_dashboard_projects_path) do
- = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
- Starred Projects
- = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do
- = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
- Explore Projects
+= content_for :flash_message do
+ = render 'shared/project_limit'
+.top-area
+ %ul.left-top-menu
+ = nav_link(page: [dashboard_projects_path, root_path]) do
+ = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
+ Your Projects
+ = nav_link(page: starred_dashboard_projects_path) do
+ = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
+ Starred Projects
+ = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do
+ = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
+ Explore Projects
+
+ .projects-search-form
+ = search_field_tag :filter_projects, nil, placeholder: 'Filter by name...', class: 'projects-list-filter form-control hidden-xs', spellcheck: false
+ - if current_user.can_create_project?
+ = link_to new_project_path, class: 'btn btn-green' do
+ %i.fa.fa-plus
+ New Project
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index c249f5cacec..d5b7e729e7b 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -8,12 +8,12 @@
= link_to new_group_path, class: "btn btn-new" do
%i.fa.fa-plus
New Group
- .title Welcome to the groups!
- Group members have access to all group projects.
+ .oneline
+ Group members have access to all group projects.
%ul.content-list
- @group_members.each do |group_member|
- group = group_member.group
= render 'shared/groups/group', group: group, group_member: group_member
-= paginate @group_members
+= paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index cd602e897b7..2d3da01178a 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -4,14 +4,20 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
+.project-issuable-filter
+ .controls
+ .pull-left
+ - if current_user
+ .hidden-xs.pull-left
+ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
+ %i.fa.fa-rss
-.append-bottom-20
- .pull-right
- - if current_user
- .hidden-xs.pull-left.prepend-top-20
- = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: '' do
- %i.fa.fa-rss
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues
-= render 'shared/issues'
+.gray-content-block.second-block
+ List all issues from all projects you have access to.
+
+.prepend-top-default
+ = render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index d1f332fa0d3..c5a5ec21f78 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,6 +1,14 @@
- page_title "Merge Requests"
- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
-.append-bottom-20
+.project-issuable-filter
+ .controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
+
= render 'shared/issuable/filter', type: :merge_requests
-= render 'shared/merge_requests'
+
+.gray-content-block.second-block
+ List all merge requests from all projects you have access to.
+
+.prepend-top-default
+ = render 'shared/merge_requests'
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index 55080d6b3fe..7c882a32702 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -16,7 +16,10 @@
= milestone_progress_bar(milestone)
.row
.col-sm-6
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name_with_namespace
+ .expiration
+ = render 'shared/milestone_expired', milestone: milestone
+ .projects
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = milestone.project.name_with_namespace
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 21b25c3986e..bec1692a4de 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,19 +1,21 @@
- page_title "Milestones"
-- header_title "Milestones", dashboard_milestones_path
+- header_title "Milestones", dashboard_milestones_path
+.project-issuable-filter
+ .controls
+ = render 'shared/new_project_item_select', path: 'milestones/new', label: "New Milestone", include_groups: true
-= render 'shared/milestones_filter'
+ = render 'shared/milestones_filter'
.gray-content-block
- .oneline
- List all milestones from all projects you have access to.
+ List all milestones from all projects you have access to.
.milestones
%ul.content-list
- - if @dashboard_milestones.blank?
+ - if @milestones.blank?
%li
.nothing-here-block No milestones to show
- else
- - @dashboard_milestones.each do |milestone|
+ - @milestones.each do |milestone|
= render 'milestone', milestone: milestone
- = paginate @dashboard_milestones, theme: "gitlab"
+ = paginate @milestones, theme: "gitlab"
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 2fe14c6388c..4316c358dcb 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,18 +1,22 @@
-- page_title @dashboard_milestone.title, "Milestones"
-%h4.page-title
- .issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" }
- - if @dashboard_milestone.closed?
+- page_title @milestone.title, "Milestones"
+- header_title "Milestones", dashboard_milestones_path
+
+.detail-page-header
+ .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
+ - if @milestone.closed?
Closed
- else
Open
- Milestone #{@dashboard_milestone.title}
+ %span.identifier
+ Milestone #{@milestone.title}
-%hr
-- if (@dashboard_milestone.total_items_count == @dashboard_milestone.closed_items_count) && @dashboard_milestone.active?
- .alert.alert-success
- %span All issues for this milestone are closed. You may close the milestone now.
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(@milestone.title), pipeline: :single_line
-.description
+- if @milestone.complete? && @milestone.active?
+ .alert.alert-success.prepend-top-default
+ %span All issues for this milestone are closed. You may close the milestone now.
.table-holder
%table.table
@@ -22,7 +26,7 @@
%th Open issues
%th State
%th Due date
- - @dashboard_milestone.milestones.each do |milestone|
+ - @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name_with_namespace}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
@@ -39,46 +43,60 @@
.context
%p.lead
Progress:
- #{@dashboard_milestone.closed_items_count} closed
+ #{@milestone.closed_items_count} closed
&ndash;
- #{@dashboard_milestone.open_items_count} open
- = milestone_progress_bar(@dashboard_milestone)
+ #{@milestone.open_items_count} open
+ = milestone_progress_bar(@milestone)
-%ul.nav.nav-tabs
+%ul.center-top-menu.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
- %span.badge= @dashboard_milestone.issue_count
+ %span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
- %span.badge= @dashboard_milestone.merge_requests_count
+ %span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
- %span.badge= @dashboard_milestone.participants.count
-
- .pull-right
- = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @dashboard_milestone.title), class: "btn edit-milestone-link btn-grouped"
+ %span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Issues', issues_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All issues in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'issues', title: "Open", issues: @dashboard_milestone.opened_issues
+ = render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
- = render 'issues', title: "Closed", issues: @dashboard_milestone.closed_issues
+ = render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Merge Requests', merge_requests_dashboard_path(milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All merge requests in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @dashboard_milestone.opened_merge_requests
+ = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @dashboard_milestone.closed_merge_requests
+ = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
+ .gray-content-block.middle-block
+ .oneline
+ All participants to this milestone
%ul.bordered-list
- - @dashboard_milestone.participants.each do |user|
+ - @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
diff --git a/app/views/dashboard/projects/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index 81a5909e2d2..cea9ffcc748 100644
--- a/app/views/dashboard/projects/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -1,11 +1,3 @@
.projects-list-holder
- .projects-search-form
- .input-group
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- - if current_user.can_create_project?
- %span.input-group-btn
- = link_to new_project_path, class: 'btn btn-green' do
- %i.fa.fa-plus
- New Project
= render 'shared/projects/list', projects: @projects, ci: true
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index d2c51486841..c8c219f4cca 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 7a16b811f6b..53abf274bdb 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -3,7 +3,7 @@
= auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index f75f2e0a32a..70705923d42 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,5 +1,5 @@
- page_title "Starred Projects"
-- header_title "Projects", projects_path
+- header_title "Projects", dashboard_projects_path
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index d3908062f43..07b6d57932e 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -6,33 +6,29 @@
.gray-content-block
.pull-right
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ = icon('plus')
+ New Snippet
- .oneline
- Share code pastes with others out of git repository
-
-%ul.nav.nav-tabs.prepend-top-20
- = nav_tab :scope, nil do
- = link_to dashboard_snippets_path do
+ .btn-group.btn-group-next.snippet-scope-menu
+ = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
All
%span.badge
= current_user.snippets.count
- = nav_tab :scope, 'are_private' do
- = link_to dashboard_snippets_path(scope: 'are_private') do
+
+ = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
Private
%span.badge
= current_user.snippets.are_private.count
- = nav_tab :scope, 'are_internal' do
- = link_to dashboard_snippets_path(scope: 'are_internal') do
+
+ = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
Internal
%span.badge
= current_user.snippets.are_internal.count
- = nav_tab :scope, 'are_public' do
- = link_to dashboard_snippets_path(scope: 'are_public') do
+
+ = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
Public
%span.badge
= current_user.snippets.are_public.count
-.my-snippets
- = render 'snippets/snippets'
+= render 'snippets/snippets'
diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb
deleted file mode 100644
index 79d6c761d8f..00000000000
--- a/app/views/devise/mailer/unlock_instructions.html.erb
+++ /dev/null
@@ -1,7 +0,0 @@
-<p>Hello <%= @resource.email %>!</p>
-
-<p>Your account has been locked due to an excessive amount of unsuccessful sign in attempts.</p>
-
-<p>Click the link below to unlock your account:</p>
-
-<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p>
diff --git a/app/views/devise/mailer/unlock_instructions.html.haml b/app/views/devise/mailer/unlock_instructions.html.haml
new file mode 100644
index 00000000000..52b327e20c5
--- /dev/null
+++ b/app/views/devise/mailer/unlock_instructions.html.haml
@@ -0,0 +1,10 @@
+%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)
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 9dc6aeffd59..cb93ff2465e 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -6,17 +6,21 @@
.login-heading
%h3 Create an account
.login-body
+ - user = params[:user].present? ? params[:user] : {}
= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f|
.devise-errors
= devise_error_messages!
%div
- = f.text_field :name, class: "form-control top", placeholder: "Name", required: true
+ = f.text_field :name, class: "form-control top", value: user[:name], placeholder: "Name", required: true
%div
- = f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
+ = f.text_field :username, class: "form-control middle", value: user[:username], placeholder: "Username", required: true
%div
- = f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
+ = f.email_field :email, class: "form-control middle", value: user[:email], placeholder: "Email", required: true
.form-group.append-bottom-20#password-strength
- = f.password_field :password, class: "form-control bottom", id: "user_password_sign_up", placeholder: "Password", required: true
+ = f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
+ %div
+ - if current_application_settings.recaptcha_enabled
+ = recaptcha_tags
%div
= f.submit "Sign up", class: "btn-create btn"
diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb
deleted file mode 100644
index f9277d1673f..00000000000
--- a/app/views/devise/unlocks/new.html.erb
+++ /dev/null
@@ -1,12 +0,0 @@
-<h2>Resend unlock instructions</h2>
-
-<%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %>
- <%= devise_error_messages! %>
-
- <div><%= f.label :email %><br />
- <%= f.email_field :email %></div>
-
- <div><%= f.submit "Resend unlock instructions" %></div>
-<% end %>
-
-<%= render partial: "devise/shared/links" %>
diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml
new file mode 100644
index 00000000000..49c087c0646
--- /dev/null
+++ b/app/views/devise/unlocks/new.html.haml
@@ -0,0 +1,14 @@
+.login-box
+ .login-heading
+ %h3 Resend unlock email
+ .login-body
+ = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f|
+ .devise-errors
+ = devise_error_messages!
+ .clearfix.append-bottom-20
+ = f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off'
+ .clearfix
+ = f.submit 'Resend unlock instructions', class: 'btn btn-success'
+
+.clearfix.prepend-top-20
+ = render 'devise/shared/sign_in_link'
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index ad63841ccf3..4ba8b84fd92 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: ''
&middot;
- = gfm event_commit_title(commit[:message]), project: project
+ = markdown event_commit_title(commit[:message]), project: project, pipeline: :single_line
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 9aacc79d686..46432a92348 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -3,7 +3,7 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- = cache [event, "v2.1"] do
+ = cache [event, current_application_settings, "v2.1"] do
= image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:''
- if event.created_project?
= render "events/event/created_project", event: event
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 2761272aa8a..28b12c8dca8 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -3,7 +3,7 @@
.form-group
= search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search", spellcheck: false
.form-group
- = button_tag 'Search', class: "btn btn-success"
+ = button_tag 'Search', class: "btn"
.pull-right.hidden-sm.hidden-xs
- if current_user
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 67e38ca3127..b9a958fbe7b 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -1,12 +1,12 @@
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
- else
= render 'explore/head'
-.gray-content-block.clearfix
+.gray-content-block.clearfix.second-block
= render 'filter'
= render 'projects', projects: @projects
= paginate @projects, theme: "gitlab"
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 596cb0a96cd..95d46e331f8 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -1,5 +1,5 @@
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
@@ -7,7 +7,7 @@
= render 'explore/head'
.explore-trending-block
- .gray-content-block
+ .gray-content-block.second-block
.pull-right
= render 'explore/projects/dropdown'
.oneline
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 5ea6d81c5b9..fa0b718e48b 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -1,5 +1,5 @@
- page_title "Projects"
-- header_title "Projects", root_path
+- header_title "Projects", dashboard_projects_path
- if current_user
= render 'dashboard/projects_head'
@@ -7,7 +7,7 @@
= render 'explore/head'
.explore-trending-block
- .gray-content-block
+ .gray-content-block.second-block
.pull-right
= render 'explore/projects/dropdown'
.oneline
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 7e4fa7d4873..0f100c39ffb 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -10,7 +10,8 @@
- if current_user
.pull-right
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ = icon('plus')
+ New Snippet
.oneline
Public snippets created by you and other users are listed here
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 11d69977ef9..bbafc08435a 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -1,5 +1,5 @@
-.panel.panel-default.projects-list-holder
- .panel-heading.clearfix
+.projects-list-holder
+ .projects-search-form
.input-group
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
- if can? current_user, :create_projects, @group
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index ae8fc9f85f0..7e3e2e28bc9 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -3,8 +3,7 @@
.panel.panel-default
.panel-heading
- %strong= @group.name
- group settings:
+ Group settings
.panel-body
= form_for @group, html: { multipart: true, class: "form-horizontal" }, authenticity_token: true do |f|
- if @group.errors.any?
@@ -36,4 +35,5 @@
%br
%strong Removed group can not be restored!
- = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove"
+ .form-actions
+ = link_to 'Remove Group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove"
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index 3c19381321a..a79a0fcdc8e 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,11 +1,10 @@
- user = member.user
- return unless user || member.invite?
-- show_roles = true if show_roles.nil?
%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, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
%strong
= link_to user.name, user_path(user)
%span.cgray= user.username
@@ -15,7 +14,7 @@
%label.label.label-danger
%strong Blocked
- else
- = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong
= member.invite_email
%span.cgray
@@ -25,18 +24,19 @@
= 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, member)
+ - 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
+ - if should_user_see_group_roles?(current_user, @group)
%span.pull-right
- %strong= member.human_access
+ %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
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index fee4b0052b5..335bf036074 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,42 +1,38 @@
- page_title "Members"
- header_title group_title(@group, "Members", group_group_members_path(@group))
-- show_roles = should_user_see_group_roles?(current_user, @group)
-
-- if show_roles
- %p.light
- Members of group have access to all group projects.
- Read more about permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-
-
-.clearfix.js-toggle-container
- = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false }
- = button_tag 'Search', class: 'btn'
+- @blank_container = true
+.group-members-page
- if current_user && current_user.can?(:admin_group_member, @group)
- .pull-right
- = button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
- Add members
- %i.fa.fa-chevron-down
-
- .js-toggle-content.hide.new-group-member-holder
- = render "new_group_member"
-
-.panel.panel-default.prepend-top-20
- .panel-heading
- %strong #{@group.name}
- group members
- %small
- (#{@members.total_count})
- %ul.well-list
- - @members.each do |member|
- = render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true
-
-= paginate @members, theme: 'gitlab'
-
-:coffeescript
- $('form.member-search-form').on 'submit', (event) ->
- event.preventDefault()
- Turbolinks.visit @.action + '?' + $(@).serialize()
+ .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.
+ .new-group-member-holder
+ = render "new_group_member"
+
+ .panel.panel-default
+ .panel-heading
+ %strong #{@group.name}
+ group members
+ %small
+ (#{@members.total_count})
+ .pull-right
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
+ = 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
+ = paginate @members, theme: 'gitlab'
+
+:javascript
+ $('form.member-search-form').on('submit', function(event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '?' + $(this).serialize());
+ });
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index 5bad48abafd..df726e2b2b9 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(@member)}").replaceWith('#{escape_javascript(render(@member, member: @member, show_controls: true))}');
+ $("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render(@group_member, member: @group_member, show_controls: true))}');
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 08d97e418a3..90ade1e1680 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -4,21 +4,24 @@
- if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
+.project-issuable-filter
+ .controls
+ .pull-left
+ - if current_user
+ .hidden-xs.pull-left
+ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
+ %i.fa.fa-rss
+ = render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
+
+ = render 'shared/issuable/filter', type: :issues
-= render 'shared/issuable/filter', type: :issues
.gray-content-block.second-block
- .pull-right
- - if current_user
- .hidden-xs.pull-left
- = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do
- %i.fa.fa-rss
- %div
- Only issues from
- %strong #{@group.name}
- group are listed here.
- - if current_user
- To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
+ Only issues from
+ %strong #{@group.name}
+ group are listed here.
+ - if current_user
+ To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
.prepend-top-default
= render 'shared/issues'
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 425ad8331bf..f662f5a8c17 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,13 +1,18 @@
- page_title "Merge Requests"
- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group))
-= render 'shared/issuable/filter', type: :merge_requests
+.project-issuable-filter
+ .controls
+ = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
+
+ = render 'shared/issuable/filter', type: :merge_requests
+
.gray-content-block.second-block
- %div
- Only merge requests from
- %strong #{@group.name}
- group are listed here.
- - if current_user
- To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
+ Only merge requests from
+ %strong #{@group.name}
+ group are listed here.
+ - if current_user
+ To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
+
.prepend-top-default
= render 'shared/merge_requests'
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index 41dffdd2fb8..a20bf75bc39 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -22,7 +22,7 @@
%span.label.label-gray
= milestone.project.name
.col-sm-6
- - if can?(current_user, :admin_group, @group)
+ - if can?(current_user, :admin_milestones, @group)
- if milestone.closed?
= link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
- else
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 2bbcad5fdfb..b221d3a89a4 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,17 +1,28 @@
- page_title "Milestones"
- header_title group_title(@group, "Milestones", group_milestones_path(@group))
-= render 'shared/milestones_filter'
+.project-issuable-filter
+ .controls
+ - if can?(current_user, :admin_milestones, @group)
+ .pull-right
+ %span.pull-right.hidden-xs
+ = link_to new_group_milestone_path(@group), class: "btn btn-new" do
+ = icon('plus')
+ New Milestone
+
+ = render 'shared/milestones_filter'
+
.gray-content-block
Only milestones from
%strong #{@group.name}
group are listed here.
+
.milestones
%ul.content-list
- - if @group_milestones.blank?
+ - if @milestones.blank?
%li
.nothing-here-block No milestones to show
- else
- - @group_milestones.each do |milestone|
+ - @milestones.each do |milestone|
= render 'milestone', milestone: milestone
- = paginate @group_milestones, theme: "gitlab"
+ = paginate @milestones, theme: "gitlab"
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
new file mode 100644
index 00000000000..3894a0ece74
--- /dev/null
+++ b/app/views/groups/milestones/new.html.haml
@@ -0,0 +1,47 @@
+- page_title "Milestones"
+- header_title group_title(@group, "Milestones", group_milestones_path(@group))
+
+%h3.page-title
+ New Milestone
+
+%p.light
+ This will create milestone in every selected project
+%hr
+
+= form_for @milestone, url: group_milestones_path(@group), html: { class: 'form-horizontal milestone-form gfm-form js-requires-input' } do |f|
+ .row
+ .col-md-6
+ .form-group
+ = f.label :title, "Title", class: "control-label"
+ .col-sm-10
+ = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
+ .form-group.milestone-description
+ = f.label :description, "Description", class: "control-label"
+ .col-sm-10
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
+ = render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
+ .clearfix
+ .error-alert
+ .form-group
+ = f.label :projects, "Projects", class: "control-label"
+ .col-sm-10
+ = f.collection_select :project_ids, @group.projects, :id, :name,
+ { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
+
+ .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
+
+ .form-actions
+ = f.submit 'Create Milestone', class: "btn-create btn"
+ = link_to "Cancel", group_milestones_path(@group), 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/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index a92ad5d751b..d063b257b5e 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,26 +1,28 @@
-- page_title @group_milestone.title, "Milestones"
+- page_title @milestone.title, "Milestones"
= render "header_title"
-%h4.page-title
- .issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
- - if @group_milestone.closed?
+.detail-page-header
+ .status-box{ class: "status-box-#{@milestone.closed? ? 'closed' : 'open'}" }
+ - if @milestone.closed?
Closed
- else
Open
- Milestone #{@group_milestone.title}
+ %span.identifier
+ Milestone #{@milestone.title}
.pull-right
- - if can?(current_user, :admin_group, @group)
- - if @group_milestone.active?
- = link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
+ - if can?(current_user, :admin_milestones, @group)
+ - if @milestone.active?
+ = link_to 'Close Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close"
- else
- = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
+ = link_to 'Reopen Milestone', group_milestone_path(@group, @milestone.safe_title, title: @milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen"
-%hr
-- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
- .alert.alert-success
- %span All issues for this milestone are closed. You may close the milestone now.
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(@milestone.title), pipeline: :single_line
-.description
+- if @milestone.complete? && @milestone.active?
+ .alert.alert-success.prepend-top-default
+ %span All issues for this milestone are closed. You may close the milestone now.
.table-holder
%table.table
@@ -30,7 +32,7 @@
%th Open issues
%th State
%th Due date
- - @group_milestone.milestones.each do |milestone|
+ - @milestone.milestones.each do |milestone|
%tr
%td
= link_to "#{milestone.project.name}", namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
@@ -47,46 +49,61 @@
.context
%p.lead
Progress:
- #{@group_milestone.closed_items_count} closed
+ #{@milestone.closed_items_count} closed
&ndash;
- #{@group_milestone.open_items_count} open
- = milestone_progress_bar(@group_milestone)
+ #{@milestone.open_items_count} open
+ = milestone_progress_bar(@milestone)
-%ul.nav.nav-tabs
+%ul.center-top-menu.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
- %span.badge= @group_milestone.issue_count
+ %span.badge= @milestone.issue_count
%li
= link_to '#tab-merge-requests', 'data-toggle' => 'tab' do
Merge Requests
- %span.badge= @group_milestone.merge_requests_count
+ %span.badge= @milestone.merge_requests_count
%li
= link_to '#tab-participants', 'data-toggle' => 'tab' do
Participants
- %span.badge= @group_milestone.participants.count
-
- .pull-right
- = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @group_milestone.title), class: "btn edit-milestone-link btn-grouped"
+ %span.badge= @milestone.participants.count
.tab-content
.tab-pane.active#tab-issues
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Issues', issues_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All issues in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'issues', title: "Open", issues: @group_milestone.opened_issues
+ = render 'issues', title: "Open", issues: @milestone.opened_issues
.col-md-6
- = render 'issues', title: "Closed", issues: @group_milestone.closed_issues
+ = render 'issues', title: "Closed", issues: @milestone.closed_issues
.tab-pane#tab-merge-requests
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ = link_to 'Browse Merge Requests', merge_requests_group_path(@group, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All merge requests in this milestone
+
+ .row.prepend-top-default
.col-md-6
- = render 'merge_requests', title: "Open", merge_requests: @group_milestone.opened_merge_requests
+ = render 'merge_requests', title: "Open", merge_requests: @milestone.opened_merge_requests
.col-md-6
- = render 'merge_requests', title: "Closed", merge_requests: @group_milestone.closed_merge_requests
+ = render 'merge_requests', title: "Closed", merge_requests: @milestone.closed_merge_requests
.tab-pane#tab-participants
+ .gray-content-block.middle-block
+ .oneline
+ All participants to this milestone
+
%ul.bordered-list
- - @group_milestone.participants.each do |user|
+ - @milestone.participants.each do |user|
%li
= link_to user, title: user.name, class: "darken" do
= image_tag avatar_icon(user, 32), class: "avatar s32"
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 0665cdf387a..4bc31cabea6 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -1,5 +1,10 @@
- page_title 'New Group'
-- header_title 'New Group'
+- header_title "Groups", dashboard_groups_path
+
+%h3.page-title
+ New Group
+%hr
+
= form_for @group, html: { class: 'group-form form-horizontal' } do |f|
- if @group.errors.any?
.alert.alert-danger
@@ -18,3 +23,4 @@
.form-actions
= f.submit 'Create group', class: "btn btn-create", tabindex: 3
+ = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index a91d1a6e94b..7ea574434c3 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group)
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index dc8e81323a6..a607d860d7d 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -5,37 +5,47 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
-.dashboard
- .header-with-avatar.clearfix
- = image_tag group_icon(@group), class: "avatar group-avatar s90"
- %h3
- = @group.name
- .username
- @#{@group.path}
- - if @group.description.present?
- .description
- = markdown(@group.description, pipeline: :description)
- %hr
-
- = render 'shared/show_aside'
-
- - if can?(current_user, :read_group, @group)
- .row
- %section.activities.col-md-7
- .hidden-xs
- - if current_user
- = render "events/event_last_push", event: @last_push
- .pull-right
- = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do
- %i.fa.fa-rss
-
- = render 'shared/event_filter'
- %hr
-
- .content_list
- = spinner
- %aside.side.col-md-5
- = render "projects", projects: @projects
- - else
- %p
- This group does not have public projects
+.cover-block
+ .avatar-holder
+ = link_to group_icon(@group), target: '_blank' do
+ = image_tag group_icon(@group), class: "avatar group-avatar s90"
+ .cover-title
+ = @group.name
+
+ .cover-desc.username
+ @#{@group.path}
+
+ - if @group.description.present?
+ .cover-desc.description
+ = markdown(@group.description, pipeline: :description)
+
+- if can?(current_user, :read_group, @group)
+ %ul.center-top-menu.no-top
+ %li.active
+ = link_to "#activity", 'data-toggle' => 'tab' do
+ Activity
+ - if @projects.present?
+ %li
+ = link_to "#projects", 'data-toggle' => 'tab' do
+ Projects
+
+ .tab-content
+ .tab-pane.active#activity
+ .gray-content-block.activity-filter-block
+ - if current_user
+ = render "events/event_last_push", event: @last_push
+ .pull-right
+ = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn' do
+ %i.fa.fa-rss
+
+ = render 'shared/event_filter'
+
+ .content_list
+ = spinner
+
+ .tab-pane#projects
+ = render "projects", projects: @projects
+
+- else
+ %p.center-top-menu.no-top
+ No projects to show
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 67349fcbd78..e8e331dd109 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -219,11 +219,3 @@
%td.shortcut
.key r
%td Reply (quoting selected text)
-
-
-:javascript
- $('.js-more-help-button').click(function(e){
- $(this).remove()
- $('.hidden-shortcut').show()
- e.preventDefault()
- });
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 2169a821fb2..d9ffda884c8 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -31,11 +31,9 @@
%h2#blocks Blocks
- %h3
+ %h4
%code .gray-content-block
-
-
.gray-content-block.middle-block
%h4 Normal block inside content
= lorem
@@ -45,9 +43,28 @@
= lorem
+ %h4
+ %code .cover-block
+ %br
+ .cover-block
+ .avatar-holder
+ = image_tag avatar_icon('admin@example.com', 90), class: "avatar s90", alt: ''
+ .cover-title
+ John Smith
+
+ .cover-desc
+ = lorem
+
+ .cover-controls
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('pencil')
+ &nbsp;
+ = link_to '#', class: 'btn btn-gray' do
+ = icon('rss')
+
%h2#lists Lists
- %h3
+ %h4
%code .content-list
%ul.content-list
%li
@@ -57,7 +74,7 @@
%li
One item
- %h3
+ %h4
%code .well-list
%ul.well-list
%li
@@ -67,7 +84,7 @@
%li
One item
- %h3
+ %h4
%code .panel .well-list
.panel.panel-default
@@ -80,7 +97,7 @@
%li
One item
- %h3
+ %h4
%code .bordered-list
%ul.bordered-list
%li
@@ -121,7 +138,7 @@
%h2#navs Navigation
- %h3
+ %h4
%code .center-top-menu
.example
%ul.center-top-menu
@@ -130,7 +147,7 @@
%li
%a Closed
- %h3
+ %h4
%code .btn-group.btn-group-next
.example
%div.btn-group.btn-group-next
@@ -138,7 +155,7 @@
%a.btn Closed
- %h3
+ %h4
%code .nav.nav-tabs
.example
%ul.nav.nav-tabs
@@ -204,7 +221,7 @@
%h2#forms Forms
- %h3
+ %h4
%code form.horizontal-form
%form.form-horizontal
@@ -226,7 +243,7 @@
.col-sm-offset-2.col-sm-10
%button.btn.btn-default{:type => "submit"} Sign in
- %h3
+ %h4
%code form
%form
@@ -243,7 +260,7 @@
%button.btn.btn-default{:type => "submit"} Sign in
%h2#file File
- %h3
+ %h4
%code .file-holder
- blob = Snippet.new(content: "Wow\nSuch\nFile")
@@ -254,13 +271,12 @@
.file-actions
.btn-group
%a.btn Edit
- %a.btn Remove
+ %a.btn.btn-danger Remove
.file-contenta.code
= render 'shared/file_highlight', blob: blob
-
%h2#markdown Markdown
- %h3
+ %h4
%code .md or .wiki and others
Markdown rendering has a bit different css and presented in next UI elements:
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 30bcdb86827..aec2e836c9f 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,4 +1,5 @@
- page_title "Bitbucket import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
@@ -66,5 +67,5 @@
again.
-:coffeescript
- new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}");
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index e1bb88ca4ed..5515fad6f48 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -1,4 +1,5 @@
- page_title "FogBugz Import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bug
Import projects from FogBugz
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index a701e49ac56..07338736bac 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -1,4 +1,5 @@
- page_title 'User map', 'FogBugz import'
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bug
Import projects from FogBugz
@@ -22,7 +23,7 @@
%strong Map a FogBugz account ID to a GitLab user
%p
Selecting a GitLab user will add a link to the GitLab user in the descriptions
- of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also
+ of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also
associate and/or assign these issues and comments with the selected user.
.table-holder
@@ -46,5 +47,5 @@
.form-actions
= submit_tag 'Continue to the next step', class: 'btn btn-create'
-:coffeescript
- new UsersSelect()
+:javascript
+ new UsersSelect();
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
index beca6ab1423..6ee16c8be4b 100644
--- a/app/views/import/fogbugz/status.html.haml
+++ b/app/views/import/fogbugz/status.html.haml
@@ -1,4 +1,5 @@
- page_title "FogBugz import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-bug
Import projects from FogBugz
@@ -48,5 +49,5 @@
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}");
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 0669b05adca..1416ee5bd5a 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,4 +1,5 @@
- page_title "GitHub import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-github
Import projects from GitHub
@@ -43,5 +44,5 @@
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}");
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 3bc85059e7d..911a55eb85d 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,4 +1,5 @@
- page_title "GitLab.com import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-heart
Import projects from GitLab.com
@@ -43,5 +44,5 @@
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}");
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index 2e3a535737f..6b0fa1edf8c 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -1,4 +1,5 @@
- page_title "Gitorious import"
+- header_title "Projects", root_path
%h3.page-title
%i.icon-gitorious.icon-gitorious-big
Import projects from Gitorious.org
@@ -43,5 +44,5 @@
%td.import-actions.job-status
= button_tag "Import", class: "btn js-add-to-import"
-:coffeescript
- new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}");
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 9c64e0a009f..5d2f149cd5f 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -1,4 +1,5 @@
- page_title "Google Code import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-google
Import projects from Google Code
@@ -6,7 +7,7 @@
= form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do
%p
- Follow the steps below to export your Google Code project data.
+ Follow the steps below to export your Google Code project data.
In the next step, you'll be able to select the projects you want to import.
%ol
%li
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
index e53ebda7dc1..0738b3db1eb 100644
--- a/app/views/import/google_code/new_user_map.html.haml
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -1,4 +1,5 @@
- page_title "User map", "Google Code import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-google
Import projects from Google Code
@@ -8,31 +9,31 @@
%p
Customize how Google Code email addresses and usernames are imported into GitLab.
In the next step, you'll be able to select the projects you want to import.
- %p
+ %p
The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side.
%ul
%li
%strong Default: Directly import the Google Code email address or username
%p
- <code>"johnsmith@example.com": "johnsm...@example.com"</code>
- will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com.
+ <code>"johnsmith@example.com": "johnsm...@example.com"</code>
+ will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com.
The email address or username is masked to ensure the user's privacy.
%li
%strong Map a Google Code user to a GitLab user
%p
- <code>"johnsmith@example.com": "@johnsmith"</code>
- will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com,
+ <code>"johnsmith@example.com": "@johnsmith"</code>
+ will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com,
and will set <a href="#">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com.
%li
%strong Map a Google Code user to a full name
%p
- <code>"johnsmith@example.com": "John Smith"</code>
+ <code>"johnsmith@example.com": "John Smith"</code>
will add "By John Smith" to all issues and comments originally created by johnsmith@example.com.
%li
%strong Map a Google Code user to a full email address
%p
- <code>"johnsmith@example.com": "johnsmith@example.com"</code>
- will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
+ <code>"johnsmith@example.com": "johnsmith@example.com"</code>
+ will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address.
.form-group
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index c5af06edf87..175ef6921cd 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -1,4 +1,5 @@
- page_title "Google Code import"
+- header_title "Projects", root_path
%h3.page-title
%i.fa.fa-google
Import projects from Google Code
@@ -67,5 +68,5 @@
= link_to "import flow", new_import_google_code_path
again.
-:coffeescript
- new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}")
+:javascript
+ new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}");
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 74174a72f5a..dd133ee8b56 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,11 +1,24 @@
-- page_title "GitLab"
-%head
+%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
- %meta{content: "GitLab Community Edition", name: "description"}
- %meta{name: 'referrer', content: 'origin-when-cross-origin'}
- %title= page_title
+ -# Open Graph - http://ogp.me/
+ %meta{property: 'og:type', content: "object"}
+ %meta{property: 'og:site_name', content: "GitLab"}
+ %meta{property: 'og:title', content: page_title}
+ %meta{property: 'og:description', content: page_description}
+ %meta{property: 'og:image', content: page_image}
+ %meta{property: 'og:url', content: request.base_url + request.fullpath}
+
+ -# Twitter Card - https://dev.twitter.com/cards/types/summary
+ %meta{property: 'twitter:card', content: "summary"}
+ %meta{property: 'twitter:title', content: page_title}
+ %meta{property: 'twitter:description', content: page_description}
+ %meta{property: 'twitter:image', content: page_image}
+ = page_card_meta_tags
+
+ %title= page_title('GitLab')
+ %meta{name: "description", content: page_description}
= favicon_link_tag 'favicon.ico'
@@ -18,6 +31,8 @@
= include_gon
+ - unless browser.safari?
+ %meta{name: 'referrer', content: 'origin-when-cross-origin'}
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
%meta{name: 'theme-color', content: '#474D57'}
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 352b8040cf4..ec7cd79bc54 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,8 +1,8 @@
-.page-with-sidebar{ class: nav_sidebar_class }
+.page-with-sidebar{ class: page_sidebar_class }
= render "layouts/broadcast"
- .sidebar-wrapper.nicescroll
+ .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
= brand_header_logo
.gitlab-text-container
%h3 GitLab
@@ -17,8 +17,8 @@
.collapse-nav
= render partial: 'layouts/collapse_button'
- if current_user
- = link_to current_user, class: 'sidebar-user' do
- = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36'
+ = 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
diff --git a/app/views/layouts/_piwik.html.haml b/app/views/layouts/_piwik.html.haml
index 135e8daca26..259b4f7cdfc 100644
--- a/app/views/layouts/_piwik.html.haml
+++ b/app/views/layouts/_piwik.html.haml
@@ -1,12 +1,14 @@
+<!-- Piwik -->
:javascript
var _paq = _paq || [];
- _paq.push(["trackPageView"]);
- _paq.push(["enableLinkTracking"]);
-
+ _paq.push(['trackPageView']);
+ _paq.push(['enableLinkTracking']);
(function() {
- var u=(("https:" == document.location.protocol) ? "https" : "http") + "://#{extra_config.piwik_url}/";
- _paq.push(["setTrackerUrl", u+"piwik.php"]);
- _paq.push(["setSiteId", "#{extra_config.piwik_site_id}"]);
- var d=document, g=d.createElement("script"), s=d.getElementsByTagName("script")[0]; g.type="text/javascript";
- g.defer=true; g.async=true; g.src=u+"piwik.js"; s.parentNode.insertBefore(g,s);
+ var u="//#{extra_config.piwik_url}/";
+ _paq.push(['setTrackerUrl', u+'piwik.php']);
+ _paq.push(['setSiteId', #{extra_config.piwik_site_id}]);
+ var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
+ g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
+<noscript><p><img src="//#{extra_config.piwik_url}/piwik.php?idsite=#{extra_config.piwik_site_id}" style="border:0;" alt="" /></p></noscript>
+<!-- End Piwik Code -->
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index ceb64ce3157..a44f5762a6b 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -11,6 +11,8 @@
= hidden_field_tag :scope, 'merge_requests'
- elsif current_controller?(:wikis)
= hidden_field_tag :scope, 'wiki_blobs'
+ - elsif current_controller?(:commits)
+ = hidden_field_tag :scope, 'commits'
- else
= hidden_field_tag :search_code, true
@@ -23,6 +25,6 @@
:javascript
$('.search-input').on('keyup', function(e) {
if (e.keyCode == 27) {
- $('.search-input').blur()
+ $('.search-input').blur();
}
- })
+ });
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 1c738719bd8..6591c52bdbd 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
+- page_title "Admin Area"
+- header_title "Admin Area", admin_root_path
- sidebar "admin"
= render template: "layouts/application"
diff --git a/app/views/layouts/ci/_nav_admin.html.haml b/app/views/layouts/ci/_nav_admin.html.haml
deleted file mode 100644
index af2545a22d8..00000000000
--- a/app/views/layouts/ci/_nav_admin.html.haml
+++ /dev/null
@@ -1,33 +0,0 @@
-%ul.nav.nav-sidebar
- = nav_link do
- = link_to admin_root_path, title: 'Back to admin', data: {placement: 'right'}, class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Back to admin
-
- %li.separate-item
- = nav_link path: 'projects#index' do
- = link_to ci_admin_projects_path do
- = icon('list-alt fw')
- Projects
- = nav_link path: 'events#index' do
- = link_to ci_admin_events_path do
- = icon('book fw')
- Events
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to ci_admin_runners_path do
- = icon('cog fw')
- Runners
- %small.pull-right
- = Ci::Runner.count(:all)
- = nav_link path: 'builds#index' do
- = link_to ci_admin_builds_path do
- = icon('link fw')
- Builds
- %small.pull-right
- = Ci::Build.count(:all)
- = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
- = link_to ci_admin_application_settings_path do
- = icon('cogs fw')
- %span
- Settings
diff --git a/app/views/layouts/ci/_nav_project.html.haml b/app/views/layouts/ci/_nav_project.html.haml
deleted file mode 100644
index f094edbfa87..00000000000
--- a/app/views/layouts/ci/_nav_project.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-%ul.nav.nav-sidebar
- = nav_link do
- = link_to project_path(@project.gl_project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do
- = icon('caret-square-o-left fw')
- %span
- Back to project
- %li.separate-item
- = nav_link path: 'events#index' do
- = link_to ci_project_events_path(@project) do
- = icon('book fw')
- %span
- Events
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
index ab3e29c3f42..7e90af21b21 100644
--- a/app/views/layouts/ci/_page.html.haml
+++ b/app/views/layouts/ci/_page.html.haml
@@ -1,8 +1,8 @@
-.page-with-sidebar{ class: nav_sidebar_class }
+.page-with-sidebar{ class: page_sidebar_class }
= render "layouts/broadcast"
- .sidebar-wrapper.nicescroll
+ .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
.header-logo
- = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home' do
= brand_header_logo
.gitlab-text-container
%h3 GitLab
@@ -14,8 +14,8 @@
.collapse-nav
= render partial: 'layouts/collapse_button'
- if current_user
- = link_to current_user, class: 'sidebar-user' do
- = image_tag avatar_icon(current_user, 60), alt: 'User activity', class: 'avatar avatar s36'
+ = 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
diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml
deleted file mode 100644
index c8cb185d28c..00000000000
--- a/app/views/layouts/ci/admin.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render 'layouts/head'
- %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
- - header_title = "Admin area"
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
- = render 'layouts/ci/page', sidebar: 'nav_admin'
diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml
deleted file mode 100644
index 38023468d0b..00000000000
--- a/app/views/layouts/ci/application.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render 'layouts/head'
- %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
- - header_title = "Continuous Integration"
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
- = render 'layouts/ci/page'
diff --git a/app/views/layouts/ci/project.html.haml b/app/views/layouts/ci/project.html.haml
deleted file mode 100644
index 15478c3f5bc..00000000000
--- a/app/views/layouts/ci/project.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render 'layouts/head'
- %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
- - header_title @project.name, ci_project_path(@project)
- - if current_user
- = render "layouts/header/default", title: header_title
- - else
- = render "layouts/header/public", title: header_title
-
- = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 95e077c339f..f08cb0a5428 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,13 +1,13 @@
!!! 5
%html{ lang: "en"}
= render "layouts/head"
- %body.ui_charcoal.login-page.application
+ %body.ui_charcoal.login-page.application.navless
= render "layouts/header/empty"
= render "layouts/broadcast"
.container.navless-container
.content
= render "layouts/flash"
- .row.prepend-top-20
+ .row
.col-sm-5.pull-right
= yield
.col-sm-7.brand-holder.pull-left
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index 2af265a2296..915acc4612e 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: "en"}
= render "layouts/head"
- %body{class: "#{user_application_theme} application"}
+ %body{class: "#{user_application_theme} application navless"}
= 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 c31b1cbe9a8..3892ef8eefa 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -11,18 +11,27 @@
%li.hidden-sm.hidden-xs
= render 'layouts/search'
%li.visible-sm.visible-xs
- = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('search')
+ - if session[:impersonator_id]
+ %li.impersonation
+ = link_to stop_impersonation_admin_users_path, method: :delete, title: 'Stop Impersonation', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = icon('user-secret fw')
- if current_user.is_admin?
%li
- = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to admin_root_path, title: 'Admin Area', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('wrench fw')
- if current_user.can_create_project?
%li
- = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('plus fw')
+ - if Gitlab::Sherlock.enabled?
+ %li
+ = link_to sherlock_transactions_path, title: 'Sherlock Transactions',
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = icon('tachometer fw')
%li
- = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('sign-out')
%h1.title= title
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 2079feeeab6..cffdb52cc23 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -5,78 +5,85 @@
%span
Overview
= nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} 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', data: {placement: 'right'} 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', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
- = nav_link do
- = link_to ci_admin_projects_path, title: 'Continuous Integration', data: {placement: 'right'} do
- = icon('building fw')
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path 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 do
+ = icon('link fw')
%span
- Continuous Integration
+ Builds
+ %span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} 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: 'Broadcast Messages', data: {placement: 'right'} 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', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
= icon('cog fw')
%span
Background Jobs
= nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} 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', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to admin_labels_path, title: 'Labels' do
= icon('tags fw')
%span
Labels
= nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse reports" do
+ = link_to admin_abuse_reports_path, title: "Abuse Reports" do
= icon('exclamation-circle fw')
%span
Abuse Reports
- %span.count= AbuseReport.count(:all)
+ %span.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', data: {placement: 'right'} do
+ = link_to admin_application_settings_path, title: 'Settings' do
= icon('cogs fw')
%span
Settings
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index b1a1d531846..106abd24a56 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,50 +1,50 @@
%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', data: {placement: 'right'} do
+ = link_to dashboard_projects_path, title: 'Projects' do
= icon('home fw')
%span
Projects
= nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity', data: {placement: 'right'} do
+ = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity' do
= icon('dashboard fw')
%span
Activity
= nav_link(controller: :groups) do
- = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = link_to dashboard_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
= nav_link(controller: :milestones) do
- = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do
+ = link_to dashboard_milestones_path, title: 'Milestones' do
= icon('clock-o fw')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do
= icon('exclamation-circle fw')
%span
Issues
- %span.count= current_user.assigned_issues.opened.count
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
= nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
= icon('tasks fw')
%span
Merge Requests
- %span.count= current_user.assigned_merge_requests.opened.count
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
= nav_link(controller: :snippets) do
- = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ = link_to dashboard_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
- = link_to help_path, title: 'Help', data: {placement: 'right'} do
+ = link_to help_path, title: 'Help' do
= icon('question-circle fw')
%span
Help
%li.separate-item
= nav_link(controller: :profile) do
- = link_to profile_path, title: 'Profile settings', data: {placement: 'bottom'} do
+ = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
= icon('user fw')
%span
Profile Settings
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
index 21e565972a7..48039ca2918 100644
--- a/app/views/layouts/nav/_explore.html.haml
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -1,21 +1,21 @@
%ul.nav.nav-sidebar
= nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
- = link_to explore_root_path, title: 'Projects', data: {placement: 'right'} do
+ = link_to explore_root_path, title: 'Projects' do
= icon('home fw')
%span
Projects
= nav_link(controller: :groups) do
- = link_to explore_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = link_to explore_groups_path, title: 'Groups' do
= icon('group fw')
%span
Groups
= nav_link(controller: :snippets) do
- = link_to explore_snippets_path, title: 'Snippets', data: {placement: 'right'} do
+ = link_to explore_snippets_path, title: 'Snippets' do
= icon('clipboard fw')
%span
Snippets
= nav_link(controller: :help) do
- = link_to help_path, title: 'Help', data: {placement: 'right'} do
+ = link_to help_path, title: 'Help' do
= icon('question-circle fw')
%span
Help
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index eb35af22b93..e5e2a59eaed 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,46 +1,46 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to dashboard
+ Go to dashboard
%li.separate-item
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
- = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do
+ = link_to group_path(@group), title: 'Home' do
= icon('dashboard fw')
%span
Group
- if can?(current_user, :read_group, @group)
- if current_user
= nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to issues_group_path(@group), title: 'Issues' do
= icon('exclamation-circle fw')
%span
Issues
- if current_user
- %span.count= Issue.opened.of_group(@group).count
+ %span.count= number_with_delimiter(Issue.opened.of_group(@group).count)
= nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= icon('tasks fw')
%span
Merge Requests
- if current_user
- %span.count= MergeRequest.opened.of_group(@group).count
+ %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count)
= nav_link(controller: [:group_members]) do
- = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do
+ = link_to group_group_members_path(@group), title: 'Members' do
= icon('users fw')
%span
Members
- if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "separate-item" }) do
- = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do
+ = link_to edit_group_path(@group), title: 'Settings' do
= icon ('cogs fw')
%span
Settings
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index 8075fe32fbc..56a92fe9103 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,20 +1,20 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to group_path(@group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do
+ = link_to group_path(@group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to group
+ Go to group
%li.separate-item
%ul.sidebar-subnav
= nav_link(path: 'groups#edit') do
- = link_to edit_group_path(@group), title: 'Group Settings', data: {placement: 'right'} do
+ = link_to edit_group_path(@group), title: 'Group Settings' do
= icon ('pencil-square-o fw')
%span
Group Settings
= nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do
+ = link_to projects_group_path(@group), title: 'Projects' do
= icon('folder fw')
%span
Projects
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 5a47b8e6db2..f3ded04419b 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,59 +1,59 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to dashboard
+ Go to dashboard
%li.separate-item
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: 'Profile', data: {placement: 'right'} do
+ = link_to profile_path, title: 'Profile Settings' do
= icon('user fw')
%span
Profile Settings
= nav_link(controller: [:accounts, :two_factor_auths]) do
- = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do
+ = link_to profile_account_path, title: 'Account' do
= icon('gear fw')
%span
Account
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new', 'applications#create']) do
- = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to profile_emails_path, title: 'Emails' do
= icon('envelope-o fw')
%span
Emails
- %span.count= current_user.emails.count + 1
+ %span.count= number_with_delimiter(current_user.emails.count + 1)
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to profile_notifications_path, title: 'Notifications' do
= icon('inbox fw')
%span
Notifications
= nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do
+ = link_to profile_keys_path, title: 'SSH Keys' do
= icon('key fw')
%span
SSH Keys
- %span.count= current_user.keys.count
+ %span.count= number_with_delimiter(current_user.keys.count)
= nav_link(controller: :preferences) do
- = link_to profile_preferences_path, title: 'Preferences', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to audit_log_profile_path, title: 'Audit Log' do
= icon('history fw')
%span
Audit Log
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 53a913fe8f3..d3eaf0f3209 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,120 +1,120 @@
%ul.nav.nav-sidebar
- if @project.group
= nav_link do
- = link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do
+ = link_to group_path(@project.group), title: 'Go to group', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to group
+ Go to group
- else
= nav_link do
- = link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
+ = link_to root_path, title: 'Go to dashboard', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to dashboard
+ 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', data: {placement: 'right'} do
+ = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
= icon('home fw')
%span
Project
= nav_link(path: 'projects#activity') do
- = link_to activity_project_path(@project), title: 'Project Activity', class: 'shortcuts-project-activity', data: {placement: 'right'} 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)) do
- = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do
+ = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do
= icon('files-o fw')
%span
Files
- if project_nav_tab? :commits
- = nav_link(controller: %w(commit commits compare repositories tags branches)) do
- = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do
+ = 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')
%span
Commits
- if project_nav_tab? :builds
= nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds', data: {placement: 'right'} do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
= icon('cubes fw')
%span
Builds
- %span.count.builds_counter= @project.ci_builds.running_or_pending.count(:all)
-
- - if project_nav_tab? :network
- = nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do
- = icon('code-fork fw')
- %span
- Network
+ %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', data: {placement: 'right'} 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', data: {placement: 'right'} 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', data: {placement: 'right'} 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= @project.issues.opened.count
+ %span.count.issue_counter= number_with_delimiter(@project.issues.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', data: {placement: 'right'} 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= @project.merge_requests.opened.count
+ %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
- = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} 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', data: {placement: 'right'} 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', data: {placement: 'right'} do
+ = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki' do
= icon('book fw')
%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', data: {placement: 'right'} 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', data: {placement: 'right'} 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
+
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 954dbe5d2b9..970da78a5c9 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -1,72 +1,52 @@
%ul.nav.nav-sidebar
= nav_link do
- = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do
+ = link_to project_path(@project), title: 'Go to project', class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to project
+ Go to project
%li.separate-item
%ul.sidebar-subnav
= nav_link(path: 'projects#edit') do
- = link_to edit_project_path(@project), title: 'Project Settings', data: {placement: 'right'} do
+ = link_to edit_project_path(@project), title: 'Project Settings' do
= icon('pencil-square-o fw')
%span
Project Settings
= nav_link(controller: :deploy_keys) do
- = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do
+ = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
= icon('key fw')
%span
Deploy Keys
= nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
= icon('link fw')
%span
Web Hooks
= nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do
+ = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
= icon('cogs fw')
%span
Services
= nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do
+ = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
= icon('lock fw')
%span
Protected Branches
- - if @project.gitlab_ci?
+ - if @project.builds_enabled?
= nav_link(controller: :runners) do
- = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} 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) do
+ = link_to namespace_project_variables_path(@project.namespace, @project), title: 'Variables' do
= icon('code fw')
%span
Variables
= nav_link path: 'triggers#index' do
- = link_to namespace_project_triggers_path(@project.namespace, @project) do
+ = link_to namespace_project_triggers_path(@project.namespace, @project), title: 'Triggers' do
= icon('retweet fw')
%span
Triggers
- = nav_link path: 'ci_web_hooks#index' do
- = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project) do
- = icon('link fw')
- %span
- CI Web Hooks
- = nav_link path: 'ci_settings#edit' do
- = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do
- = icon('building fw')
- %span
- CI Settings
- = nav_link controller: 'ci_services' do
- = link_to namespace_project_ci_services_path(@project.namespace, @project) do
- = icon('share fw')
- %span
- CI Services
- = nav_link path: 'events#index' do
- = link_to ci_project_events_path(@project.gitlab_ci_project) do
- = icon('book fw')
- %span
- CI Events
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 854cda57c39..3ca4c340406 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -40,9 +40,10 @@
Reply to this email directly or
#{link_to "view it on GitLab", @target_url}.
- else
- #{link_to "View it on GitLab", @target_url}
+ #{link_to "View it on GitLab", @target_url}.
%br
- You're receiving this email because of your account on #{link_to Gitlab.config.gitlab.host, root_url}.
+ -# Don't link the host is the line below, one link in the email is easier to quickly click than two.
+ You're receiving this email because of your account on #{Gitlab.config.gitlab.host}.
If you'd like to receive fewer emails, you can adjust your notification settings.
- = email_action @target_url
+ = email_action @target_url \ No newline at end of file
diff --git a/app/views/ci/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
index 69689a75022..f4e9749e5c7 100644
--- a/app/views/ci/notify/build_fail_email.html.haml
+++ b/app/views/notify/build_fail_email.html.haml
@@ -1,19 +1,23 @@
- content_for :header do
%h1{style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
- GitLab CI (build failed)
+ GitLab (build failed)
%h3
Project:
= link_to ci_project_url(@project) do
= @project.name
%p
- Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+ Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
%p
Author: #{@build.commit.git_author_name}
%p
Branch: #{@build.ref}
%p
+ Stage: #{@build.stage}
+%p
+ Job: #{@build.name}
+%p
Message: #{@build.commit.git_commit_message}
%p
- Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)}
+ Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/ci/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
index 6de5dc10f17..675acea60a1 100644
--- a/app/views/ci/notify/build_fail_email.text.erb
+++ b/app/views/notify/build_fail_email.text.erb
@@ -4,6 +4,8 @@ Status: <%= @build.status %>
Commit: <%= @build.commit.short_sha %>
Author: <%= @build.commit.git_author_name %>
Branch: <%= @build.ref %>
+Stage: <%= @build.stage %>
+Job: <%= @build.name %>
Message: <%= @build.commit.git_commit_message %>
-Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %>
+Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/ci/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
index 4e3015a356b..8b004d34cca 100644
--- a/app/views/ci/notify/build_success_email.html.haml
+++ b/app/views/notify/build_success_email.html.haml
@@ -1,6 +1,6 @@
- content_for :header do
%h1{style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;"}
- GitLab CI (build successful)
+ GitLab (build successful)
%h3
Project:
@@ -8,13 +8,17 @@
= @project.name
%p
- Commit link: #{gitlab_commit_link(@project, @build.commit.short_sha)}
+ Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
%p
Author: #{@build.commit.git_author_name}
%p
Branch: #{@build.ref}
%p
+ Stage: #{@build.stage}
+%p
+ Job: #{@build.name}
+%p
Message: #{@build.commit.git_commit_message}
%p
- Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build)}
+ Build details: #{link_to "Build #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/ci/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
index d0a43ae1c12..747da44acae 100644
--- a/app/views/ci/notify/build_success_email.text.erb
+++ b/app/views/notify/build_success_email.text.erb
@@ -4,6 +4,8 @@ Status: <%= @build.status %>
Commit: <%= @build.commit.short_sha %>
Author: <%= @build.commit.git_author_name %>
Branch: <%= @build.ref %>
+Stage: <%= @build.stage %>
+Job: <%= @build.name %>
Message: <%= @build.commit.git_commit_message %>
-Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %>
+Url: <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb
index d8a23dabf49..b2c5f71e465 100644
--- a/app/views/notify/project_was_moved_email.text.erb
+++ b/app/views/notify/project_was_moved_email.text.erb
@@ -1,4 +1,4 @@
-Project #{@old_path_with_namespace} was moved to another location
+Project <%= @old_path_with_namespace %> was moved to another location
The project is now located under
<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 12f83aae04b..4361f67a74d 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -1,30 +1,32 @@
-%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
+%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))}
-- if @compare
- - if @reverse_compare
+- if @message.compare
+ - if @message.reverse_compare?
%p
%strong WARNING:
The push did not contain any new commits, but force pushed to delete the commits and changes below.
%h4
- = @reverse_compare ? "Deleted commits:" : "Commits:"
+ = @message.reverse_compare? ? "Deleted commits:" : "Commits:"
%ul
- - @commits.each do |commit|
+ - @message.commits.each do |commit|
%li
- %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)}
+ %strong #{link_to(commit.short_id, namespace_project_commit_url(@message.project_namespace, @message.project, commit))}
%div
%span by #{commit.author_name}
%i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
%pre.commit-message
= commit.safe_message
- %h4 #{pluralize @diffs.count, "changed file"}:
+ %h4 #{pluralize @message.diffs_count, "changed file"}:
%ul
- - @diffs.each_with_index do |diff, i|
+ - @message.diffs.each_with_index do |diff, i|
%li.file-stats
- %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" }
+ %a{href: "#{@message.target_url if @message.disable_diffs?}#diff-#{i}" }
- if diff.deleted_file
%span.deleted-file
&minus;
@@ -40,11 +42,11 @@
- else
= diff.new_path
- - unless @disable_diffs
+ - unless @message.disable_diffs?
%h4 Changes:
- - @diffs.each_with_index do |diff, i|
+ - @message.diffs.each_with_index do |diff, i|
%li{id: "diff-#{i}"}
- %a{href: @target_url + "#diff-#{i}"}
+ %a{href: @message.target_url + "#diff-#{i}"}
- if diff.deleted_file
%strong
= diff.old_path
@@ -62,5 +64,5 @@
= color_email_diff(diff.diff)
%br
- - if @compare.timeout
+ - if @message.compare_timeout
%h5 Huge diff. To prevent performance issues changes are hidden
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 97a176ed2a3..aa0e263b6df 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -1,21 +1,21 @@
-#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace}
-- if @compare
+#{@message.author_name} #{@message.action_name} #{@message.ref_type} #{@message.ref_name} at #{@message.project_name_with_namespace}
+- if @message.compare
\
\
- - if @reverse_compare
+ - if @message.reverse_compare?
WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below.
\
\
- = @reverse_compare ? "Deleted commits:" : "Commits:"
- - @commits.each do |commit|
+ = @message.reverse_compare? ? "Deleted commits:" : "Commits:"
+ - @message.commits.each do |commit|
#{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
#{commit.safe_message}
\- - - - -
\
\
- #{pluralize @diffs.count, "changed file"}:
+ #{pluralize @message.diffs_count, "changed file"}:
\
- - @diffs.each do |diff|
+ - @message.diffs.each do |diff|
- if diff.deleted_file
\- − #{diff.old_path}
- elsif diff.renamed_file
@@ -24,11 +24,11 @@
\- + #{diff.new_path}
- else
\- #{diff.new_path}
- - unless @disable_diffs
+ - unless @message.disable_diffs?
\
\
Changes:
- - @diffs.each do |diff|
+ - @message.diffs.each do |diff|
\
\=====================================
- if diff.deleted_file
@@ -39,11 +39,11 @@
= diff.new_path
\=====================================
!= diff.diff
- - if @compare.timeout
+ - if @message.compare_timeout
\
\
Huge diff. To prevent performance issues it was hidden
- - if @target_url
+ - if @message.target_url
\
\
- View it on GitLab: #{@target_url}
+ View it on GitLab: #{@message.target_url}
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index cd7b1b0fe03..17e47c622ce 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -23,11 +23,14 @@
%p.cgray
- if current_user.private_token
= text_field_tag "token", current_user.private_token, class: "form-control"
- %div
- = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default btn-build-token"
- else
%span You don`t have one yet. Click generate to fix it.
- = f.submit 'Generate', class: "btn btn-default btn-build-token"
+
+ .form-actions
+ - if current_user.private_token
+ = f.submit 'Reset private token', data: { confirm: "Are you sure?" }, class: "btn btn-default"
+ - else
+ = f.submit 'Generate', class: "btn btn-default"
- unless current_user.ldap_user?
.panel.panel-default
@@ -54,7 +57,8 @@
%p
Each time you log in you’ll be required to provide your username and
password as usual, plus a randomly-generated code from your phone.
- %div
+
+ .form-actions
= link_to 'Enable Two-factor Authentication', new_profile_two_factor_auth_path, class: 'btn btn-success'
- if button_based_providers.any?
@@ -81,15 +85,16 @@
%p
Changing your username will change path to all personal projects!
%div
- = f.text_field :username, required: true, class: 'form-control'
+ .input-group
+ .input-group-addon
+ = "#{root_url}u/"
+ = f.text_field :username, required: true, class: 'form-control'
&nbsp;
.loading-gif.hide
%p
= icon('spinner spin')
Saving new username
- %p.light
- = user_url(@user)
- %div
+ .form-actions
= f.submit 'Save username', class: "btn btn-warning"
- if signup_enabled?
@@ -104,7 +109,8 @@
- rp = current_user.personal_projects.count
- unless rp.zero?
%li #{pluralize rp, 'personal project'} will be removed and cannot be restored
- = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
+ .form-actions
+ = link_to 'Delete account', user_registration_path, data: { confirm: "REMOVE #{current_user.name}? Are you sure?" }, method: :delete, class: "btn btn-remove"
- else
- if @user.solo_owned_groups.present?
%p
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
index 2342936a5d5..0436c2213da 100644
--- a/app/views/profiles/applications.html.haml
+++ b/app/views/profiles/applications.html.haml
@@ -15,24 +15,25 @@
.pull-right
= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
- if @applications.any?
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Callback URL
- %th Clients
- %th
- %th
- %tbody
- - @applications.each do |application|
- %tr{:id => "application_#{application.id}"}
- %td= link_to application.name, oauth_application_path(application)
- %td
- - application.redirect_uri.split.each do |uri|
- %div= uri
- %td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
- %td= render 'doorkeeper/applications/delete_form', application: application
+ .table-holder
+ %table.table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Callback URL
+ %th Clients
+ %th
+ %th
+ %tbody
+ - @applications.each do |application|
+ %tr{:id => "application_#{application.id}"}
+ %td= link_to application.name, oauth_application_path(application)
+ %td
+ - application.redirect_uri.split.each do |uri|
+ %div= uri
+ %td= application.access_tokens.count
+ %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
+ %td= render 'doorkeeper/applications/delete_form', application: application
.oauth-authorized-applications.prepend-top-20
- if user_oauth_applications?
@@ -40,29 +41,30 @@
Authorized applications
- if @authorized_tokens.any?
- %table.table.table-striped
- %thead
- %tr
- %th Name
- %th Authorized At
- %th Scope
- %th
- %tbody
- - @authorized_apps.each do |app|
- - token = app.authorized_tokens.order('created_at desc').first
- %tr{:id => "application_#{app.id}"}
- %td= app.name
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', application: app
- - @authorized_anonymous_tokens.each do |token|
+ .table-holder
+ %table.table.table-striped
+ %thead
%tr
- %td
- Anonymous
- %div.help-block
- %em Authorization was granted by entering your username and password in the application.
- %td= token.created_at
- %td= token.scopes
- %td= render 'doorkeeper/authorized_applications/delete_form', token: token
+ %th Name
+ %th Authorized At
+ %th Scope
+ %th
+ %tbody
+ - @authorized_apps.each do |app|
+ - token = app.authorized_tokens.order('created_at desc').first
+ %tr{:id => "application_#{app.id}"}
+ %td= app.name
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'doorkeeper/authorized_applications/delete_form', application: app
+ - @authorized_anonymous_tokens.each do |token|
+ %tr
+ %td
+ Anonymous
+ %div.help-block
+ %em Authorization was granted by entering your username and password in the application.
+ %td= token.created_at
+ %td= token.scopes
+ %td= render 'doorkeeper/authorized_applications/delete_form', token: token
- else
%p.light You don't have any authorized applications
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index b76a5b636ac..2a8800de60e 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [:profile, @key], html: { class: 'form-horizontal' } do |f|
+ = form_for [:profile, @key], html: { class: 'form-horizontal js-requires-input' } do |f|
- if @key.errors.any?
.alert.alert-danger
%ul
@@ -9,12 +9,11 @@
.form-group
= f.label :key, class: 'control-label'
.col-sm-10
- = f.text_area :key, class: "form-control", rows: 8
+ = f.text_area :key, class: "form-control", rows: 8, autofocus: true, required: true
.form-group
= f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, class: "form-control"
+ .col-sm-10= f.text_field :title, class: "form-control", required: true
.form-actions
= f.submit 'Add key', class: "btn btn-create"
= link_to "Cancel", profile_keys_path, class: "btn btn-cancel"
-
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index ef0075aad3b..8c9d546af4c 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,6 +1,6 @@
- is_admin = defined?(admin) ? true : false
-.panel.panel-default
- - if @keys.any?
+- if @keys.any?
+ .table-holder
%table.table
%thead.panel-heading
%tr
@@ -11,9 +11,9 @@
%tbody
- @keys.each do |key|
= render 'profiles/keys/key', key: key, is_admin: is_admin
- - else
- .nothing-here-block
- - if is_admin
- User has no ssh keys
- - else
- There are no SSH keys with access to your account.
+- else
+ .nothing-here-block
+ - if is_admin
+ User has no ssh keys
+ - else
+ There are no SSH keys with access to your account.
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 14adba1c797..17a4195030e 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -3,7 +3,9 @@
.gray-content-block.top-block
.pull-right
- = link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new"
+ = link_to new_profile_key_path, class: "btn btn-new" do
+ = icon('plus')
+ Add SSH Key
.oneline
Before you can add an SSH key you need to
= link_to "generate it.", help_page_path("ssh", "README")
diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml
index 2bf207a3221..13a18269d11 100644
--- a/app/views/profiles/keys/new.html.haml
+++ b/app/views/profiles/keys/new.html.haml
@@ -9,9 +9,9 @@
$('#key_key').on('focusout', function(){
var title = $('#key_title'),
val = $('#key_key').val(),
- comment = val.match(/^\S+ \S+ (.+)$/);
+ comment = val.match(/^\S+ \S+ (.+)\n?$/);
if( comment && comment.length > 1 && title.val() == '' ){
- $('#key_title').val( comment[1] );
+ $('#key_title').val( comment[1] ).change();
}
});
diff --git a/app/views/profiles/notifications/_settings.html.haml b/app/views/profiles/notifications/_settings.html.haml
index 2c85d2a9b2b..742c5c4b68d 100644
--- a/app/views/profiles/notifications/_settings.html.haml
+++ b/app/views/profiles/notifications/_settings.html.haml
@@ -14,4 +14,4 @@
= form_tag profile_notifications_path, method: :put, remote: true, class: 'update-notifications' do
= hidden_field_tag :notification_type, type, id: dom_id(membership, 'notification_type')
= hidden_field_tag :notification_id, membership.id, id: dom_id(membership, 'notification_id')
- = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'trigger-submit'
+ = select_tag :notification_level, options_for_select(Notification.options_with_labels, notification.level), class: 'form-control trigger-submit'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 8eebd96b674..0bcadc965fa 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -50,12 +50,10 @@
Watch
%p You will receive notifications for any activity
- .form-actions
+ .gray-content-block
= f.submit 'Save changes', class: "btn btn-create"
-.clearfix
- %hr
-.row.all-notifications
+.row.all-notifications.prepend-top-default
.col-md-6
%p
You can also specify notification level per group or per project.
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index cc41d7dd813..877589dc390 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -54,4 +54,4 @@
.help-block
Choose what content you want to see on a project's home page.
.panel-footer
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index ac7355dde1f..9459d8a6295 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -43,7 +43,7 @@
.form-group
= f.label :public_email, class: "control-label"
.col-sm-10
- = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control"
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show on profile'}, class: "select2"
%span.help-block This email will be displayed on your public profile.
.form-group
= f.label :skype, class: "control-label"
@@ -96,8 +96,6 @@
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- .row
- .col-md-7
- .form-group
- .col-sm-offset-2.col-sm-10
- = f.submit 'Save changes', class: "btn btn-success"
+ .form-actions
+ = f.submit 'Save changes', class: "btn btn-success"
+ = link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
diff --git a/app/views/profiles/two_factor_auths/new.html.haml b/app/views/profiles/two_factor_auths/new.html.haml
index 92dc58c10d7..1a5b6efce35 100644
--- a/app/views/profiles/two_factor_auths/new.html.haml
+++ b/app/views/profiles/two_factor_auths/new.html.haml
@@ -38,3 +38,4 @@
= text_field_tag :pin_code, nil, class: "form-control", required: true, autofocus: true
.form-actions
= submit_tag 'Submit', class: 'btn btn-success'
+ = link_to 'Configure it later', skip_profile_two_factor_auth_path, :method => :patch, class: 'btn btn-cancel' if two_factor_skippable?
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 012858f70b4..101880bd105 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -8,5 +8,5 @@
.content_list{:"data-href" => activity_project_path(@project)}
= spinner
-:coffeescript
- new Activities()
+:javascript
+ new Activities();
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index 35f7e7bb34b..640612ca433 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -1,6 +1,8 @@
.form-actions
- .commit-button-annotation
- = button_tag 'Commit Changes',
- class: 'btn commit-btn js-commit-button btn-create'
+ = button_tag 'Commit Changes', class: 'btn commit-btn js-commit-button btn-create'
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
+
+ - unless can?(current_user, :push_code, @project)
+ .inline.prepend-left-10
+ = commit_in_fork_help
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 8c0980369fd..0f61e623396 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,5 +1,5 @@
- empty_repo = @project.empty_repo?
-.project-home-panel.clearfix{:class => ("empty-project" if 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')
.project-home-desc
@@ -12,23 +12,45 @@
Forked from
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
+ .cover-controls.left
+ .visibility-level-label.has_tooltip{title: project_visibility_level_description(@project.visibility_level), data: { container: 'body' } }
+ = visibility_level_icon(@project.visibility_level, fw: false)
+ = visibility_level_label(@project.visibility_level)
-
+ .cover-controls
+ - if 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
- .split-one
+ .split-one.count-buttons
= render 'projects/buttons/star'
+ = render 'projects/buttons/fork'
- - unless empty_repo
- = render 'projects/buttons/fork'
-
= render "shared/clone_panel"
- .split-repo-buttons
- - unless empty_repo
- - if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
- = icon('download fw')
-
+
+ .split-repo-buttons
+ = render "projects/buttons/download"
= render 'projects/buttons/dropdown'
+
= render 'projects/buttons/notifications'
-
+
+:coffeescript
+ new Star() \ No newline at end of file
diff --git a/app/views/projects/_last_commit.html.haml b/app/views/projects/_last_commit.html.haml
index d7b20bfc6b1..386d72e7787 100644
--- a/app/views/projects/_last_commit.html.haml
+++ b/app/views/projects/_last_commit.html.haml
@@ -3,10 +3,10 @@
- if ci_commit
= link_to ci_status_path(ci_commit), class: "ci-status ci-#{ci_commit.status}" do
= ci_status_icon(ci_commit)
- = ci_commit.status
+ = ci_status_label(ci_commit)
= 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.id), class: "commit-row-message"
+ = 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/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 7b21095ea3e..54c818baaf4 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -5,20 +5,21 @@
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
%li
- %a.js-md-preview-button(href="md-preview-holder" tabindex="-1")
+ %a.js-md-preview-button(href="#md-preview-holder" tabindex="-1")
Preview
- - if defined?(referenced_users) && referenced_users
- %span.referenced-users.pull-left.hide
+ %div
+ .md-write-holder
+ = yield
+ .md.md-preview-holder.hide
+ .js-md-preview{class: (preview_class if defined?(preview_class))}
+
+ - if defined?(referenced_users) && referenced_users
+ %div.referenced-users.hide
+ %span
= icon('exclamation-triangle')
You are about to add
%strong
%span.js-referenced-users-count 0
people
to the discussion. Proceed with caution.
-
- %div
- .md-write-holder
- = yield
- .md.md-preview-holder.hide
- .js-md-preview{class: (preview_class if defined?(preview_class))}
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 0a1cecfdcdf..d1191928d4f 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -1,23 +1,22 @@
- if readme = @repository.readme
- %article.file-holder.readme-holder
- .file-title
- = blob_icon readme.mode, readme.name
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
- %strong
- = readme.name
+ %article.readme-holder
+ .pull-right
+ - if can?(current_user, :push_code, @project)
+ = link_to icon('pencil'), namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light edit-project-readme'
.file-content.wiki
= cache(readme_cache_key) do
= render_readme(readme)
- else
- %h3.page-title
- This project does not have README yet
- - if can?(current_user, :push_code, @project)
- %p.slead
- A
- %code README
- file contains information about other files in a repository and is commonly
- distributed with computer software, forming part of its documentation.
- %br
- We recommend you to
- = link_to "add README", new_readme_path, class: 'underlined-link'
- file to the repository and GitLab will render it here instead of this message.
+ .gray-content-block.second-block.center
+ %h3.page-title
+ This project does not have README yet
+ - if can?(current_user, :push_code, @project)
+ %p
+ A
+ %code README
+ file contains information about other files in a repository and is commonly
+ distributed with computer software, forming part of its documentation.
+ %p
+ We recommend you to
+ = link_to "add README", new_readme_path, class: 'underlined-link'
+ file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 63ebfc9381f..7e6301abde8 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -2,9 +2,12 @@
%input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop
- classes << ' js-gfm-input markdown-area'
- = f.text_area attr, class: classes, placeholder: ''
+ - if defined?(f) && f
+ = f.text_area attr, class: classes, placeholder: ''
+ - else
+ = text_area_tag attr, nil, class: classes, placeholder: ''
%a.zen-enter-link(tabindex="-1" href="#")
- %i.fa.fa-expand
+ = icon('expand')
Edit in fullscreen
%a.zen-leave-link(href="#")
- %i.fa.fa-compress
+ = icon('compress')
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 6518c4173e1..8d9ec068a43 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -6,7 +6,7 @@
#tree-holder.tree-holder
.file-holder
.file-title
- %i.fa.fa-file
+ = blob_icon @blob.mode, @blob.name
%strong
= @path
%small= number_to_human_size @blob.size
@@ -43,4 +43,3 @@
- blame_group[:lines].each do |line|
:erb
<%= highlight(@blob.name, line, nowrap: true, continue: true).html_safe %>
-
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index 373b3a0c5b0..cdac50f7a8d 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,9 +1,8 @@
.btn-group.tree-btn-group
- = edit_blob_link(@project, @ref, @path)
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
class: 'btn btn-sm', target: '_blank'
-# only show normal/blame view links for text files
- - if @blob.text?
+ - if blob_text_viewable?(@blob)
- if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
= link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
@@ -12,11 +11,11 @@
class: 'btn btn-sm' unless @blob.empty?
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
class: 'btn btn-sm'
- - if @ref != @commit.sha
- = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-sm'
+ = link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm'
-- if allowed_tree_edit?
+- if current_user
.btn-group{ role: "group" }
- %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
- %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove
+ = edit_blob_link
+ = replace_blob_link
+ = delete_blob_link
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 42f632b38ef..2a3315da3db 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -29,10 +29,12 @@
%strong
= blob.name
%small
- = number_to_human_size(blob.size)
+ = number_to_human_size(blob_size(blob))
.file-actions.hidden-xs
= render "actions"
- - if blob.text?
+ - if blob.lfs_pointer?
+ = render "download", blob: blob
+ - elsif blob.text?
= render "text", blob: blob
- elsif blob.image?
= render "image", blob: blob
diff --git a/app/views/projects/blob/_download.html.haml b/app/views/projects/blob/_download.html.haml
index f2c5e95ecf4..7908fcae3de 100644
--- a/app/views/projects/blob/_download.html.haml
+++ b/app/views/projects/blob/_download.html.haml
@@ -4,4 +4,4 @@
%h1.light
%i.fa.fa-download
%h4
- Download (#{number_to_human_size blob.size})
+ Download (#{number_to_human_size blob_size(blob)})
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index f1ad0c3c403..10b02813733 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -1,23 +1,22 @@
-.file-holder.file
- .file-title
+.file-holder.file.append-bottom-default
+ .file-title.clearfix
.editor-ref
- %i.fa.fa-code-fork
+ = icon('code-fork')
= ref
%span.editor-file-name
- - if @path
- %span.monospace
- = @path
+ = @path
- - if current_action?(:new) || current_action?(:create)
+ - if current_action?(:new) || current_action?(:create)
+ %span.editor-file-name
\/
- = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
- required: true, class: 'form-control new-file-name js-quick-submit'
- .pull-right
- = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'form-control'
+ = text_field_tag 'file_name', params[:file_name], placeholder: "File name",
+ required: true, class: 'form-control new-file-name js-quick-submit'
+
+ .pull-right
+ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-content.code
- %pre.js-edit-mode-pane#editor
- = params[:content] || local_assigns[:blob_data]
+ %pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
- if local_assigns[:path]
.js-edit-mode-pane#preview.hide
.center
diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml
index cb1567a2e68..084608bbba3 100644
--- a/app/views/projects/blob/_new_dir.html.haml
+++ b/app/views/projects/blob/_new_dir.html.haml
@@ -5,21 +5,21 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title Create New Directory
.modal-body
- = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, id: 'dir-create-form', class: 'form-horizontal' do
+ = form_tag namespace_project_create_dir_path(@project.namespace, @project, @id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
.form-group
- = label_tag :dir_name, 'Directory Name', class: 'control-label'
+ = label_tag :dir_name, 'Directory name', class: 'control-label'
.col-sm-10
- = text_field_tag :dir_name, params[:dir_name], placeholder: "Directory name", required: true, class: 'form-control'
- = render 'shared/commit_message_container', params: params, placeholder: ''
- - unless @project.empty_repo?
- .form-group
- = label_tag :branch_name, 'Branch', class: 'control-label'
- .col-sm-10
- = text_field_tag 'new_branch', @ref, class: "form-control"
- .form-group
- .col-sm-offset-2.col-sm-10
- = submit_tag "Create directory", class: 'btn btn-primary btn-create'
- = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+ = text_field_tag :dir_name, params[:dir_name], required: true, class: 'form-control'
+
+ = render 'shared/new_commit_form', placeholder: "Add new directory"
+
+ .form-actions
+ = submit_tag "Create directory", class: 'btn btn-create'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+ - unless can?(current_user, :push_code, @project)
+ .inline.prepend-left-10
+ = commit_in_fork_help
-:coffeescript
- disableButtonIfAnyEmptyField($("#dir-create-form"), ".form-control", ".btn-create");
+:javascript
+ new NewCommitForm($('.js-create-dir-form'))
diff --git a/app/views/projects/blob/_remove.html.haml b/app/views/projects/blob/_remove.html.haml
index cae5ff01099..1cf19a7d3db 100644
--- a/app/views/projects/blob/_remove.html.haml
+++ b/app/views/projects/blob/_remove.html.haml
@@ -3,16 +3,16 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title Remove #{@blob.name}
- %p.light
- From branch
- %strong= @ref
+ %h3.page-title Delete #{@blob.name}
.modal-body
- = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-requires-input' do
- = render 'shared/commit_message_container', params: params,
- placeholder: 'Removed this file because...'
+ = form_tag namespace_project_blob_path(@project.namespace, @project, @id), method: :delete, class: 'form-horizontal js-replace-blob-form js-requires-input' do
+ = render 'shared/new_commit_form', placeholder: "Delete #{@blob.name}"
+
.form-group
.col-sm-offset-2.col-sm-10
- = button_tag 'Remove file', class: 'btn btn-remove btn-remove-file'
+ = button_tag 'Delete file', class: 'btn btn-remove btn-remove-file'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:javascript
+ new NewCommitForm($('.js-replace-blob-form'))
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index e27f1707527..676924dc6ca 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -5,7 +5,7 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title #{title}
.modal-body
- = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do
+ = form_tag form_path, method: method, class: 'js-upload-blob-form form-horizontal' do
.dropzone
.dropzone-previews.blob-upload-dropzone-previews
%p.dz-message.light
@@ -13,19 +13,19 @@
= link_to 'click to upload', '#', class: "markdown-selector"
%br
.dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
- = render 'shared/commit_message_container', params: params,
- placeholder: placeholder
- - unless @project.empty_repo?
- .form-group.branch
- = label_tag 'branch', class: 'control-label' do
- Branch
- .col-sm-10
- = text_field_tag 'new_branch', @ref, class: "form-control"
- .form-group
- .col-sm-offset-2.col-sm-10
- = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all'
- = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
-:coffeescript
- disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'
- new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}')
+ = render 'shared/new_commit_form', placeholder: placeholder
+
+ .form-actions
+ = button_tag button_title, class: 'btn btn-small btn-create btn-upload-file', id: 'submit-all'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+ - unless can?(current_user, :push_code, @project)
+ .inline.prepend-left-10
+ = commit_in_fork_help
+
+
+:javascript
+ disableButtonIfEmptyField($('.js-upload-blob-form').find('.js-commit-message'), '.btn-upload-file');
+ new BlobFileDropzone($('.js-upload-blob-form'), '#{method}');
+ new NewCommitForm($('.js-upload-blob-form'))
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a811adc5094..09fa148b129 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -5,28 +5,23 @@
%ul.center-top-menu.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
- %i.fa.fa-edit
- Edit file
+ = icon('edit')
+ Edit File
%li
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
- %i.fa.fa-eye
+ = icon('eye')
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-requires-input js-edit-blob-form') do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
- = render 'shared/commit_message_container', params: params, placeholder: "Update #{@blob.name}"
-
- .form-group.branch
- = label_tag 'branch', class: 'control-label' do
- Branch
- .col-sm-10
- = text_field_tag 'new_branch', @ref, class: "form-control"
+ = render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit', @last_commit
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
- = render 'projects/commit_button', ref: @ref, cancel_path: @after_edit_path
+ = render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
:javascript
blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
+ new NewCommitForm($('.js-edit-blob-form'))
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 7975137c37f..167fa615182 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,21 +1,13 @@
- page_title "New File", @path.presence, @ref
= render "header_title"
-.gray-content-block.top-block
- Create a new file
+%h3.page-title
+ New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-requires-input') do
= render 'projects/blob/editor', ref: @ref
- = render 'shared/commit_message_container', params: params,
- placeholder: 'Add new file'
-
- - unless @project.empty_repo?
- .form-group.branch
- = label_tag 'branch', class: 'control-label' do
- Branch
- .col-sm-10
- = text_field_tag 'new_branch', @ref, class: "form-control js-quick-submit"
+ = render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
@@ -23,3 +15,4 @@
:javascript
blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null)
+ new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index f52b89f6921..6988039b6c7 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -6,10 +6,8 @@
%div#tree-holder.tree-holder
= render 'blob', blob: @blob
-- if allowed_tree_edit?
+- 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
+ = 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 cc0ec9483d2..a234536723e 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,19 +1,23 @@
- commit = @repository.commit(branch.target)
+- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
+- diverging_commit_counts = @repository.diverging_commit_counts(branch)
+- number_commits_behind = diverging_commit_counts[:behind]
+- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
%strong.str-truncated= branch.name
- &nbsp;
- - if branch.name == @repository.root_ref
- %span.label.label-primary default
- - elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}")
- merged
+ &nbsp;
+ - if branch.name == @repository.root_ref
+ %span.label.label-primary default
+ - elsif @repository.merged_to_root_ref? branch.name
+ %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}")
+ merged
- - if @project.protected_branch? branch.name
- %span.label.label-success
- %i.fa.fa-lock
- protected
+ - if @project.protected_branch? branch.name
+ %span.label.label-success
+ %i.fa.fa-lock
+ 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
@@ -26,9 +30,20 @@
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', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
+ - if branch.name != @repository.root_ref
+ .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= number_commits_behind
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= number_commits_ahead
+
+
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
index 68326e65d85..9fe65cbb104 100644
--- a/app/views/projects/branches/_commit.html.haml
+++ b/app/views/projects/branches/_commit.html.haml
@@ -1,5 +1,5 @@
-.branch-commit.light
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+.branch-commit
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-id monospace"
&middot;
%span.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 03ade02a0c8..204def60794 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -5,7 +5,7 @@
.pull-right
- if can? current_user, :push_code, @project
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
- %i.fa.fa-add-sign
+ = icon('plus')
New branch
&nbsp;
.dropdown.inline
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index f5577042ca4..c659af6338c 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -6,25 +6,25 @@
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
= @error
%h3.page-title
- %i.fa.fa-code-fork
- New branch
-= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-requires-input" do
+ New Branch
+%hr
+
+= form_tag namespace_project_branches_path, method: :post, id: "new-branch-form", class: "form-horizontal js-create-branch-form js-requires-input" do
.form-group
- = label_tag :branch_name, 'Name for new branch', class: 'control-label'
+ = label_tag :branch_name, nil, class: 'control-label'
.col-sm-10
- = text_field_tag :branch_name, params[:branch_name], placeholder: 'enter new branch name', required: true, tabindex: 1, class: 'form-control'
+ = text_field_tag :branch_name, params[:branch_name], required: true, tabindex: 1, autofocus: true, class: 'form-control js-branch-name'
+ .help-block.text-danger.js-branch-name-error
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
- = text_field_tag :ref, params[:ref], placeholder: 'existing branch name, tag or commit SHA', required: true, tabindex: 2, class: 'form-control'
+ = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+ .help-block Existing branch name, tag, or commit SHA
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_branches_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
- var availableTags = #{@project.repository.ref_names.to_json};
+ var availableRefs = #{@project.repository.ref_names.to_json};
- $("#ref").autocomplete({
- source: availableTags,
- minLength: 1
- });
+ new NewBranchForm($('.js-create-branch-form'), availableRefs)
diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml
deleted file mode 100644
index 4ce4ed63b40..00000000000
--- a/app/views/projects/builds/_build.html.haml
+++ /dev/null
@@ -1,53 +0,0 @@
-%tr.build
- %td.status
- = ci_status_with_icon(build.status)
-
- %td.commit_status-link
- - if build.target_url
- = link_to build.target_url do
- %strong Build ##{build.id}
- - else
- %strong Build ##{build.id}
-
- - if build.show_warning?
- %i.fa.fa-warning.text-warning
-
- %td
- = link_to build.short_sha, namespace_project_commit_path(@project.namespace, @project, build.sha)
-
- %td
- = link_to build.ref, namespace_project_commits_path(@project.namespace, @project, build.ref)
-
- %td
- - if build.runner
- = runner_link(build.runner)
- - else
- .light none
-
- %td
- = build.name
-
- .pull-right
- - if build.tags.any?
- - build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if build.trigger_request
- %span.label.label-info triggered
- - if build.allow_failure
- %span.label.label-danger allowed to fail
-
- %td.duration
- - if build.duration
- #{duration_in_words(build.finished_at, build.started_at)}
-
- %td.timestamp
- - if build.finished_at
- %span #{time_ago_in_words build.finished_at} ago
-
- %td
- .pull-right
- - if current_user && can?(current_user, :manage_builds, @project)
- - if build.cancel_url
- = link_to build.cancel_url, title: 'Cancel' do
- %i.fa.fa-remove.cred
diff --git a/app/views/projects/builds/_header_title.html.haml b/app/views/projects/builds/_header_title.html.haml
new file mode 100644
index 00000000000..082dab1f5b0
--- /dev/null
+++ b/app/views/projects/builds/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Builds", project_builds_path(@project))
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index e08556673ed..2fa5ad80fda 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -1,31 +1,34 @@
- page_title "Builds"
-- header_title project_title(@project, "Builds", project_builds_path(@project))
+= render "header_title"
.project-issuable-filter
.controls
- - if @ci_project && current_user && can?(current_user, :manage_builds, @project)
+ - if can?(current_user, :manage_builds, @project)
.pull-left.hidden-xs
- if @all_builds.running_or_pending.any?
- = link_to 'Cancel all', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger'
+ = link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
%ul.center-top-menu
%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= @all_builds.running_or_pending.count(:id)
+ %span.badge.js-running-count
+ = number_with_delimiter(@all_builds.running_or_pending.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to project_builds_path(@project, scope: :finished) do
Finished
- %span.badge.js-running-count= @all_builds.finished.count(:id)
-
- %li{class: ('active' if @scope == 'all')}
- = link_to project_builds_path(@project, scope: :all) do
- All
- %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+ %span.badge.js-running-count
+ = number_with_delimiter(@all_builds.finished.count(:id))
.gray-content-block
- List of #{@scope || 'running'} builds from this project
+ #{(@scope || 'running').capitalize} builds from this project
%ul.content-list
- if @builds.blank?
@@ -37,17 +40,16 @@
%thead
%tr
%th Status
- %th Build ID
+ %th Runner
%th Commit
%th Ref
- %th Runner
+ %th Stage
%th Name
%th Duration
%th Finished at
%th
- @builds.each do |build|
- = render 'projects/builds/build', build: build
-
- = paginate @builds
+ = render 'projects/commit_statuses/commit_status', commit_status: build, commit_sha: true, stage: true, allow_retry: true
+ = paginate @builds, theme: 'gitlab'
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index c45bfb27b8f..5b7ecce86ab 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -1,14 +1,20 @@
+- page_title "#{@build.name} (##{@build.id})", "Builds"
+= render "header_title"
+
.build-page
- .gray-content-block
- Build for commit
- %strong.monospace
- = link_to @build.commit.short_sha, ci_status_path(@build.commit)
+ .gray-content-block.top-block
+ Build ##{@build.id} for commit
+ %strong.monospace= link_to @build.commit.short_sha, ci_status_path(@build.commit)
from
- %code #{@build.ref}
+ = 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.iid}", merge_request_path(merge_request)
#up-build-trace
- if @commit.matrix_for_ref?(@build.ref)
- %ul.center-top-menu.build-top-menu
+ %ul.center-top-menu.no-top.no-bottom
- @commit.latest_builds_for_ref(@build.ref).each do |build|
%li{class: ('active' if build == @build) }
= link_to namespace_project_build_path(@project.namespace, @project, build) do
@@ -19,8 +25,7 @@
- else
= build.id
-
- - unless @commit.latest_builds_for_ref(@build.ref).include?(@build)
+ - if @build.retried?
%li.active
%a
Build ##{@build.id}
@@ -28,7 +33,7 @@
%i.fa.fa-warning
This build was retried.
- .gray-content-block.second-block
+ .gray-content-block.middle-block
.build-head
.clearfix
= ci_status_with_icon(@build.status)
@@ -37,7 +42,7 @@
%i.fa.fa-time
#{duration_in_words(@build.finished_at, @build.started_at)}
.pull-right
- = @build.updated_at.stamp('19:00 Aug 27')
+ #{time_ago_with_tooltip(@build.finished_at) if @build.finished_at}
- if @build.show_warning?
- unless @build.any_runners_online?
@@ -55,7 +60,7 @@
%br
Go to
- = link_to namespace_project_runners_path(@build.gl_project.namespace, @build.gl_project) do
+ = link_to namespace_project_runners_path(@build.project.namespace, @build.project) do
Runners page
.row.prepend-top-default
@@ -84,16 +89,19 @@
Test coverage
%h1 #{@build.coverage}%
+ - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
+ .build-widget.center
+ = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
.build-widget
%h4.title
- Build
+ Build ##{@build.id}
- if current_user && can?(current_user, :manage_builds, @project)
.pull-right
- - if @build.active?
- = link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-danger'
- - elsif @build.commands.present?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-primary', method: :post
+ - if @build.cancel_url
+ = link_to "Cancel", @build.cancel_url, class: 'btn btn-sm btn-danger', method: :post
+ - elsif @build.retry_url
+ = link_to "Retry", @build.retry_url, class: 'btn btn-sm btn-primary', method: :post
- if @build.duration
%p
@@ -101,15 +109,15 @@
#{duration_in_words(@build.finished_at, @build.started_at)}
%p
%span.attr-name Created:
- #{time_ago_in_words(@build.created_at)} ago
+ #{time_ago_with_tooltip(@build.created_at)}
- if @build.finished_at
%p
%span.attr-name Finished:
- #{time_ago_in_words(@build.finished_at)} ago
+ #{time_ago_with_tooltip(@build.finished_at)}
%p
%span.attr-name Runner:
- if @build.runner && current_user && current_user.admin
- \#{link_to "##{@build.runner.id}", ci_admin_runner_path(@build.runner.id)}
+ = link_to "##{@build.runner.id}", admin_runner_path(@build.runner.id)
- elsif @build.runner
\##{@build.runner.id}
@@ -134,10 +142,11 @@
%h4.title
Commit
.pull-right
- %small #{build_commit_link @build}
+ %small
+ = link_to @build.commit.short_sha, ci_status_path(@build.commit), class: "monospace"
%p
%span.attr-name Branch:
- #{build_ref_link @build}
+ = link_to @build.ref, namespace_project_commits_path(@project.namespace, @project, @build.ref)
%p
%span.attr-name Author:
#{@build.commit.git_author_name}
@@ -155,14 +164,16 @@
- if @builds.present?
.build-widget
- %h4.title #{pluralize(@builds.count, "other build")} for #{@build.short_sha}:
+ %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
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
- if build.name
= build.name
- else
@@ -171,8 +182,5 @@
%td.status= build.status
- = paginate @builds
-
-
:javascript
- new CiBuild("#{namespace_project_build_path(@project.namespace, @project, @build)}", "#{@build.status}")
+ new CiBuild("#{namespace_project_build_url(@project.namespace, @project, @build)}", "#{@build.status}")
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
new file mode 100644
index 00000000000..14ee2263b7d
--- /dev/null
+++ b/app/views/projects/buttons/_download.html.haml
@@ -0,0 +1,4 @@
+- unless @project.empty_repo?
+ - if can? current_user, :download_code, @project
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn has_tooltip', rel: 'nofollow', title: "Download ZIP" do
+ = icon('download')
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 4580c912692..f9ab78e7874 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -5,12 +5,13 @@
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
%li
- = link_to url_for_new_issue do
+ = link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - if can?(current_user, :create_merge_request, @project)
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
%li
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- if can?(current_user, :create_snippet, @project)
@@ -18,9 +19,14 @@
= link_to new_namespace_project_snippet_path(@project.namespace, @project) do
= icon('file-text-o fw')
New snippet
+
- if can?(current_user, :push_code, @project)
%li.divider
%li
+ = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
+ = icon('file fw')
+ New file
+ %li
= link_to new_namespace_project_branch_path(@project.namespace, @project) do
= icon('code-fork fw')
New branch
@@ -28,5 +34,20 @@
= link_to new_namespace_project_tag_path(@project.namespace, @project) do
= icon('tags fw')
New tag
-
-
+ - elsif current_user && current_user.already_forked?(@project)
+ %li.divider
+ %li
+ = link_to namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master') do
+ = icon('file fw')
+ New file
+ - elsif can?(current_user, :fork_project, @project)
+ %li.divider
+ %li
+ - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @project.default_branch || 'master'),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('file fw')
+ New file
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 8f2f631eb7d..133531887a2 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -1,12 +1,18 @@
-- if current_user && can?(current_user, :fork_project, @project)
- - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn' do
- = icon('code-fork fw')
- Fork
- %span.count
- = @project.forks_count
- - else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do
- = icon('code-fork fw')
- %span.count
- = @project.forks_count
+- unless @project.empty_repo?
+ - if current_user && can?(current_user, :fork_project, @project)
+ - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has_tooltip' do
+ = icon('code-fork fw')
+ Fork
+ %div.count-with-arrow
+ %span.arrow
+ %span.count
+ = @project.forks_count
+ - else
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has_tooltip' do
+ = icon('code-fork fw')
+ Fork
+ %div.count-with-arrow
+ %span.arrow
+ %span.count
+ = @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
index 0c298844912..3e83ec3912f 100644
--- a/app/views/projects/buttons/_notifications.html.haml
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -5,7 +5,7 @@
= hidden_field_tag :notification_id, @membership.id
= hidden_field_tag :notification_level
%span.dropdown
- %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-new.btn.notifications-btn#notifications-button{href: '#', "data-toggle" => "dropdown"}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
@@ -14,7 +14,7 @@
= notification_list_item(level, @membership)
- when GroupMember
- .btn.btn-new.disabled.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
+ .btn.disabled.notifications-btn.has_tooltip{title: "To change the notification level, you need to be a member of the project itself, not only its group."}
= icon('bell')
= notification_label(@membership)
= icon('angle-down')
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 3501dddefbe..21ba426aaa1 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,17 +1,21 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do
- = icon('star fw')
- %span.count
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star has_tooltip', method: :post, remote: true, title: "Star project" do
+ - if current_user.starred?(@project)
+ = icon('star fw')
+ %span.starred Unstar
+ - else
+ = icon('star-o fw')
+ %span Star
+ %div.count-with-arrow
+ %span.arrow
+ %span.count.star-count
= @project.star_count
- :coffeescript
- $('.project-home-panel .toggle-star').on 'ajax:success', (e, data, status, xhr) ->
- $(@).replaceWith(data.html)
- .on 'ajax:error', (e, xhr, status, error) ->
- new Flash('Star toggle failed. Try again later.', 'alert')
-
- else
= link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star fw')
+ Star
+ %div.count-with-arrow
+ %span.arrow
%span.count
= @project.star_count
diff --git a/app/views/projects/ci_services/_form.html.haml b/app/views/projects/ci_services/_form.html.haml
deleted file mode 100644
index 397832e56db..00000000000
--- a/app/views/projects/ci_services/_form.html.haml
+++ /dev/null
@@ -1,54 +0,0 @@
-%h3.page-title
- = @service.title
- = boolean_to_icon @service.activated?
-
-%p= @service.description
-
-
-%hr
-
-= form_for(@service, as: :service, url: namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
- - if @service.errors.any?
- .alert.alert-danger
- %ul
- - @service.errors.full_messages.each do |msg|
- %li= msg
-
- - if @service.help.present?
- .bs-callout
- = @service.help
-
- .form-group
- = f.label :active, "Active", class: "control-label"
- .col-sm-10
- = f.check_box :active
-
- - @service.fields.each do |field|
- - name = field[:name]
- - label = field[:label] || name
- - value = @service.send(name)
- - type = field[:type]
- - placeholder = field[:placeholder]
- - choices = field[:choices]
- - default_choice = field[:default_choice]
- - help = field[:help]
-
- .form-group
- = f.label label, class: "control-label"
- .col-sm-10
- - if type == 'text'
- = f.text_field name, class: "form-control", placeholder: placeholder
- - elsif type == 'textarea'
- = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
- - elsif type == 'checkbox'
- = f.check_box name
- - elsif type == 'select'
- = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- - if help
- .light #{help}
-
- .form-actions
- = f.submit 'Save', class: 'btn btn-save'
- &nbsp;
- - if @service.valid? && @service.activated? && @service.can_test?
- = link_to 'Test settings', test_namespace_project_ci_service_path(@project.namespace, @project, @service.to_param), class: 'btn'
diff --git a/app/views/projects/ci_services/edit.html.haml b/app/views/projects/ci_services/edit.html.haml
deleted file mode 100644
index bcc5832792f..00000000000
--- a/app/views/projects/ci_services/edit.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render 'form'
diff --git a/app/views/projects/ci_services/index.html.haml b/app/views/projects/ci_services/index.html.haml
deleted file mode 100644
index c164b2d4bc0..00000000000
--- a/app/views/projects/ci_services/index.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-%h3.page-title Project services
-%p.light Project services allow you to integrate GitLab CI with other applications
-
-%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_ci_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
diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml
deleted file mode 100644
index d711413c6b9..00000000000
--- a/app/views/projects/ci_settings/_form.html.haml
+++ /dev/null
@@ -1,119 +0,0 @@
-%h3.page-title
- CI settings
-%hr
-.bs-callout.help-callout
- %p
- If you want to test your .gitlab-ci.yml, you can use special tool - #{link_to "Lint", ci_lint_path}
- %p
- Edit your
- #{link_to ".gitlab-ci.yml using web-editor", yaml_web_editor_link(@ci_project)}
-
-- unless @project.empty_repo?
- %p
- Paste build status image for #{@repository.root_ref} with next link
- = link_to '#', class: 'badge-codes-toggle btn btn-default btn-xs' do
- Status Badge
- .badge-codes-block.bs-callout.bs-callout-info.hide
- %p
- Status badge for
- %span.label.label-info #{@ref}
- branch
- %div
- %label Markdown:
- = text_field_tag 'badge_md', markdown_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control'
- %label Html:
- = text_field_tag 'badge_html', html_badge_code(@ci_project, @repository.root_ref), readonly: true, class: 'form-control'
-
-= nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
- - if @ci_project.errors.any?
- #error_explanation
- %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
- .alert.alert-error
- %ul
- - @ci_project.errors.full_messages.each do |msg|
- %li= msg
-
- %fieldset
- %legend Build settings
- .form-group
- = label_tag nil, class: 'control-label' do
- Get code
- .col-sm-10
- %p Get recent application code using the following command:
- .radio
- = label_tag do
- = f.radio_button :allow_git_fetch, 'false'
- %strong git clone
- .light Slower but makes sure you have a clean dir before every build
- .radio
- = label_tag do
- = f.radio_button :allow_git_fetch, 'true'
- %strong git fetch
- .light Faster
- .form-group
- = f.label :timeout_in_minutes, 'Timeout', class: 'control-label'
- .col-sm-10
- = f.number_field :timeout_in_minutes, class: 'form-control', min: '0'
- .light per build in minutes
-
-
- %fieldset
- %legend Build Schedule
- .form-group
- = f.label :always_build, 'Schedule build', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.label :always_build do
- = f.check_box :always_build
- %span.light Repeat last build after X hours if no builds
- .form-group
- = f.label :polling_interval, "Build interval", class: 'control-label'
- .col-sm-10
- = f.number_field :polling_interval, placeholder: '5', min: '0', class: 'form-control'
- .light In hours
-
- %fieldset
- %legend Project settings
- .form-group
- = f.label :default_ref, "Make tabs for the following branches", class: 'control-label'
- .col-sm-10
- = f.text_field :default_ref, class: 'form-control', placeholder: 'master, stable'
- .light You will be able to filter builds by the following branches
- .form-group
- = f.label :public, 'Public mode', class: 'control-label'
- .col-sm-10
- .checkbox
- = f.label :public do
- = f.check_box :public
- %span.light Anyone can see project and builds
- .form-group
- = f.label :coverage_regex, "Test coverage parsing", class: 'control-label'
- .col-sm-10
- .input-group
- %span.input-group-addon /
- = f.text_field :coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
- %span.input-group-addon /
- .light 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+\%$
-
-
-
- %fieldset
- %legend Advanced settings
- .form-group
- = f.label :token, "CI token", class: 'control-label'
- .col-sm-10
- = f.text_field :token, class: 'form-control', placeholder: 'xEeFCaDAB89'
-
- .form-actions
- = f.submit 'Save changes', class: 'btn btn-save'
- - unless @ci_project.new_record?
- = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right'
diff --git a/app/views/projects/ci_settings/_no_runners.html.haml b/app/views/projects/ci_settings/_no_runners.html.haml
deleted file mode 100644
index 33038c52978..00000000000
--- a/app/views/projects/ci_settings/_no_runners.html.haml
+++ /dev/null
@@ -1,8 +0,0 @@
-.alert.alert-danger
- %p
- There are NO runners to build this project.
- %br
- You can add Specific runner for this project on Runners page
-
- - if current_user.admin
- or add Shared runner for whole application in admin are.
diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml
deleted file mode 100644
index eedf484bf00..00000000000
--- a/app/views/projects/ci_settings/edit.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- if @ci_project.generated_yaml_config
- %p.alert.alert-danger
- CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_project)}
- or
- %a.preview-yml{:href => "#yaml-content", "data-toggle" => "modal"} preview
- yaml file which is based on your old jobs.
- Put this file to the root of your project and name it .gitlab-ci.yml
-
-- if no_runners_for_project?(@ci_project)
- = render 'no_runners'
-
-= render 'form'
-
-- if @ci_project.generated_yaml_config
- #yaml-content.modal.fade{"aria-hidden" => "true", "aria-labelledby" => ".gitlab-ci.yml", :role => "dialog", :tabindex => "-1"}
- .modal-dialog
- .modal-content
- .modal-header
- %button.close{"aria-hidden" => "true", "data-dismiss" => "modal", :type => "button"} ×
- %h4.modal-title Content of .gitlab-ci.yml
- .modal-body
- = text_area_tag :yaml, @ci_project.generated_yaml_config, size: "70x25", class: "form-control"
- .modal-footer
- %button.btn.btn-default{"data-dismiss" => "modal", :type => "button"} Close
diff --git a/app/views/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml
deleted file mode 100644
index 369086b39ed..00000000000
--- a/app/views/projects/ci_web_hooks/index.html.haml
+++ /dev/null
@@ -1,93 +0,0 @@
-%h3.page-title
- CI Web hooks
-
-%p.light
- Web Hooks can be used for binding events when build completed.
-
-%hr.clearfix
-
-= form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
- -if @web_hook.errors.any?
- .alert.alert-danger
- - @web_hook.errors.full_messages.each do |msg|
- %p= msg
- .form-group
- = f.label :url, "URL", class: 'control-label'
- .col-sm-10
- = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
- .form-actions
- = f.submit "Add Web Hook", class: "btn btn-create"
-
--if @web_hooks.any?
- %h4 Activated web hooks (#{@web_hooks.count})
- .table-holder
- %table.table
- - @web_hooks.each do |hook|
- %tr
- %td
- .clearfix
- %span.monospace= hook.url
- %td
- .pull-right
- - if @ci_project.commits.any?
- = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
- = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
-
-%h4 Web Hook data example
-
-:erb
- <pre>
- <code>
- {
- "build_id": 2,
- "build_name":"rspec_linux"
- "build_status": "failed",
- "build_started_at": "2014-05-05T18:01:02.563Z",
- "build_finished_at": "2014-05-05T18:01:07.611Z",
- "project_id": 1,
- "project_name": "Brightbox \/ Brightbox Cli",
- "gitlab_url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli",
- "ref": "master",
- "sha": "a26cf5de9ed9827746d4970872376b10d9325f40",
- "before_sha": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
- "push_data": {
- "before": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
- "after": "a26cf5de9ed9827746d4970872376b10d9325f40",
- "ref": "refs\/heads\/master",
- "user_id": 1,
- "user_name": "Administrator",
- "project_id": 5,
- "repository": {
- "name": "Brightbox Cli",
- "url": "dzaporozhets@localhost:brightbox\/brightbox-cli.git",
- "description": "Voluptatibus quae error consectetur voluptas dolores vel excepturi possimus.",
- "homepage": "http:\/\/localhost:3000\/brightbox\/brightbox-cli"
- },
- "commits": [
- {
- "id": "a26cf5de9ed9827746d4970872376b10d9325f40",
- "message": "Release v1.2.2",
- "timestamp": "2014-04-22T16:46:42+03:00",
- "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/a26cf5de9ed9827746d4970872376b10d9325f40",
- "author": {
- "name": "Paul Thornthwaite",
- "email": "tokengeek@gmail.com"
- }
- },
- {
- "id": "34f57f6ba3ed0c21c5e361bbb041c3591411176c",
- "message": "Fix server user data update\n\nIncorrect condition was being used so Base64 encoding option was having\nopposite effect from desired.",
- "timestamp": "2014-04-11T18:17:26+03:00",
- "url": "http:\/\/localhost:3000\/brightbox\/brightbox-cli\/commit\/34f57f6ba3ed0c21c5e361bbb041c3591411176c",
- "author": {
- "name": "Paul Thornthwaite",
- "email": "tokengeek@gmail.com"
- }
- }
- ],
- "total_commits_count": 2,
- "ci_yaml_file":"rspec_linux:\r\n script: ls\r\n"
- }
- }
- </code>
- </pre>
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
new file mode 100644
index 00000000000..329aaa0bb8b
--- /dev/null
+++ b/app/views/projects/commit/_builds.html.haml
@@ -0,0 +1,68 @@
+.gray-content-block.middle-block
+ .pull-right
+ - if can?(current_user, :manage_builds, @ci_commit.project)
+ - if @ci_commit.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: 'btn btn-grouped btn-primary', method: :post
+
+ - if @ci_commit.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_builds_namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+
+ .oneline
+ = pluralize @statuses.count(:id), "build"
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to @ci_commit.short_sha, namespace_project_commit_path(@ci_commit.project.namespace, @ci_commit.project, @ci_commit.sha), class: "monospace"
+ - if @ci_commit.duration > 0
+ 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 @ci_commit.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 Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_commit.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ - @ci_commit.refs.each do |ref|
+ = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
+ locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true, allow_retry: true }
+
+- if @ci_commit.retried.any?
+ .gray-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 Duration
+ %th Finished at
+ - if @ci_commit.project.build_coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
+ locals: { coverage: @ci_commit.project.build_coverage_enabled?, stage: true }
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index a634ae5dfda..f74f8b427ec 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,7 +1,9 @@
-%ul.center-top-menu.commit-ci-menu
+%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
- = nav_link(path: 'commit#ci') do
- = link_to ci_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ %span.badge= @diffs.count
+ = nav_link(path: 'commit#builds') do
+ = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Builds
+ %span.badge= @statuses.count
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index fbf0a9ec0c3..ddb77fd796b 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -13,13 +13,15 @@
- 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-primary btn-grouped" do
- %span Browse Code »
+ = link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-grouped" do
+ = icon('files-o')
+ Browse Files
%div
%p
%span.light Commit
- = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit)
+ = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
+ = clipboard_button(clipboard_text: @commit.id)
.commit-info-row
%span.light Authored by
%strong
@@ -36,24 +38,24 @@
.commit-info-row
%span.cgray= pluralize(@commit.parents.count, "parent")
- @commit.parents.each do |parent|
- = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent)
+ = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
- if @ci_commit
.pull-right
= link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do
= ci_status_icon(@ci_commit)
build:
- = @ci_commit.status
+ = ci_status_label(@ci_commit)
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
.commit-box.gray-content-block.middle-block
%h3.commit-title
- = gfm escape_once(@commit.title)
+ = markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
%pre.commit-description
- = preserve(gfm(escape_once(@commit.description)))
+ = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
-:coffeescript
- $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}")
+:javascript
+ $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
new file mode 100644
index 00000000000..99d62503a94
--- /dev/null
+++ b/app/views/projects/commit/builds.html.haml
@@ -0,0 +1,6 @@
+- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/header_title"
+= render "commit_box"
+= render "ci_menu"
+
+= render "builds"
diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml
deleted file mode 100644
index 43033cad24c..00000000000
--- a/app/views/projects/commit/ci.html.haml
+++ /dev/null
@@ -1,69 +0,0 @@
-- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
-= render "projects/commits/header_title"
-= render "commit_box"
-= render "ci_menu"
-
-
-- 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
-
-- unless @ci_commit.ci_yaml_file
- .bs-callout.bs-callout-warning
- \.gitlab-ci.yml not found in this commit
-
-.gray-content-block.second-block
- Latest builds
-
- .pull-right
- - if @ci_commit.duration > 0
- %i.fa.fa-time
- #{time_interval_in_words @ci_commit.duration}
-
- &nbsp;
-
- - if @ci_project && current_user && can?(current_user, :manage_builds, @project)
- - if @ci_commit.builds.running_or_pending.any?
- = link_to "Cancel all", cancel_builds_namespace_project_commit_path(@project.namespace, @project, @commit.sha), class: 'btn btn-xs btn-danger'
-
-.table-holder
- %table.table.builds
- %thead
- %tr
- %th Status
- %th Build ID
- %th Ref
- %th Stage
- %th Name
- %th Duration
- %th Finished at
- - if @ci_project && @ci_project.coverage_enabled?
- %th Coverage
- %th
- - @ci_commit.refs.each do |ref|
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.statuses.for_ref(ref).latest.ordered,
- locals: { coverage: @ci_project.try(:coverage_enabled?), allow_retry: true }
-
-- if @ci_commit.retried.any?
- .gray-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 Duration
- %th Finished at
- - if @ci_project && @ci_project.coverage_enabled?
- %th Coverage
- %th
- = render partial: "projects/commit_statuses/commit_status", collection: @ci_commit.retried,
- locals: { coverage: @ci_project.try(:coverage_enabled?) }
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 30a3973828f..069b8b1f169 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,6 +1,9 @@
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "projects/commits/header_title"
= render "commit_box"
-= render "ci_menu" if @ci_commit
+- if @ci_commit
+ = render "ci_menu"
+- else
+ %div.block-connector
= render "projects/diffs/diffs", diffs: @diffs, project: @project
-= render "projects/notes/notes_with_form", view: params[:view]
+= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
index 637154f56aa..74a05df24d3 100644
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ b/app/views/projects/commit_statuses/_commit_status.html.haml
@@ -1,25 +1,46 @@
%tr.commit_status
%td.status
- = ci_status_with_icon(commit_status.status)
+ - if commit_status.target_url
+ = link_to commit_status.target_url, class: "ci-status ci-#{commit_status.status}" do
+ = ci_icon_for_status(commit_status.status)
+ = commit_status.status
+ - else
+ = ci_status_with_icon(commit_status.status)
%td.commit_status-link
- if commit_status.target_url
= link_to commit_status.target_url do
- %strong Build ##{commit_status.id}
+ %strong ##{commit_status.id}
- else
- %strong Build ##{commit_status.id}
+ %strong ##{commit_status.id}
- if commit_status.show_warning?
%i.fa.fa-warning.text-warning
+ - if defined?(commit_sha) && commit_sha
+ %td
+ = link_to commit_status.short_sha, namespace_project_commit_path(commit_status.project.namespace, commit_status.project, commit_status.sha), class: "monospace"
+
%td
- = commit_status.ref
+ - if commit_status.ref
+ = link_to commit_status.ref, namespace_project_commits_path(commit_status.project.namespace, commit_status.project, commit_status.ref)
+ - else
+ .light none
- %td
- = commit_status.stage
+ - if defined?(runner) && runner
+ %td
+ - if commit_status.try(:runner)
+ = runner_link(commit_status.runner)
+ - else
+ .light none
+
+ - if defined?(stage) && stage
+ %td
+ = commit_status.stage
%td
= commit_status.name
+
.pull-right
- if commit_status.tags.any?
- commit_status.tags.each do |tag|
@@ -36,7 +57,7 @@
%td.timestamp
- if commit_status.finished_at
- %span #{time_ago_in_words commit_status.finished_at} ago
+ %span #{time_ago_with_tooltip(commit_status.finished_at)}
- if defined?(coverage) && coverage
%td.coverage
@@ -45,10 +66,14 @@
%td
.pull-right
- - if current_user && can?(current_user, :manage_builds, commit_status.gl_project)
- - if commit_status.cancel_url
- = link_to commit_status.cancel_url, title: 'Cancel' do
- %i.fa.fa-remove.cred
+ - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
+ = link_to commit_status.download_url, title: 'Download artifacts' do
+ %i.fa.fa-download
+ - if current_user && can?(current_user, :manage_builds, commit_status.project)
+ - if commit_status.active?
+ - if commit_status.cancel_url
+ = link_to commit_status.cancel_url, method: :post, title: 'Cancel' do
+ %i.fa.fa-remove.cred
- elsif defined?(allow_retry) && allow_retry && commit_status.retry_url
= link_to commit_status.retry_url, method: :post, title: 'Retry' do
%i.fa.fa-repeat
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index cddd5aa3a83..012825f0fdb 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,11 +5,11 @@
- note_count = notes.user.count
- ci_commit = project.ci_commit(commit.sha)
-- cache_key = [project.path_with_namespace, commit.id, note_count]
+- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(ci_commit.status) if ci_commit
= cache(cache_key) do
- %li.commit.js-toggle-container
+ %li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
%strong.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
@@ -18,9 +18,9 @@
.pull-right
- if ci_commit
- = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do
- = ci_status_icon(ci_commit)
+ = render_ci_status(ci_commit)
&nbsp;
+ = clipboard_button(clipboard_text: commit.id)
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
.notes_count
@@ -32,7 +32,7 @@
- if commit.description?
.commit-row-description.js-toggle-content
%pre
- = preserve(gfm(escape_once(commit.description)))
+ = preserve(markdown(escape_once(commit.description), pipeline: :single_line))
.commit-row-info
= commit_author_link(commit, avatar: true, size: 24)
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index a849bf84698..fcccb002d7e 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -3,6 +3,11 @@
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
%span.badge= number_with_delimiter(@repository.commit_count)
+
+ = nav_link(controller: %w(network)) do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
+ Network
+
= nav_link(controller: :compare) do
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
Compare
@@ -12,7 +17,7 @@
Branches
%span.badge.js-totalbranch-count= @repository.branches.size
- = nav_link(controller: :tags) do
+ = nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
%span.badge.js-totaltags-count= @repository.tags.length
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 3854ad5d611..7ffa7317196 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -12,12 +12,12 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.title truncate(commit.title, length: 80)
xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
- xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email)
+ 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 gfm(commit.description)
+ xml.summary markdown(commit.description, pipeline: :single_line)
end
end
end
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 39755efd2fd..51088a7dea8 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -7,11 +7,11 @@
= render "form"
- if @commits.present?
- .prepend-top-20
+ .prepend-top-default
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, project: @project
- else
- .light-well.prepend-top-20
+ .light-well.prepend-top-default
.center
%h4
There isn't anything to compare.
diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml
index 91675b3738e..5e182af2669 100644
--- a/app/views/projects/deploy_keys/_form.html.haml
+++ b/app/views/projects/deploy_keys/_form.html.haml
@@ -1,5 +1,5 @@
%div
- = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal' } do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @key], url: namespace_project_deploy_keys_path, html: { class: 'deploy-key-form form-horizontal js-requires-input' } do |f|
-if @key.errors.any?
.alert.alert-danger
%ul
@@ -8,16 +8,15 @@
.form-group
= f.label :title, class: "control-label"
- .col-sm-10= f.text_field :title, class: 'form-control'
+ .col-sm-10= f.text_field :title, class: 'form-control', autofocus: true, required: true
.form-group
= f.label :key, class: "control-label"
.col-sm-10
%p.light
Paste a machine public key here. Read more about how to generate it
= link_to "here", help_page_path("ssh", "README")
- = f.text_area :key, class: "form-control thin_area", rows: 5
+ = f.text_area :key, class: "form-control thin_area", rows: 5, required: true
.form-actions
- = f.submit 'Create', class: "btn-create btn"
+ = f.submit 'Create Deploy Key', class: "btn-create btn"
= link_to "Cancel", namespace_project_deploy_keys_path(@project.namespace, @project), class: "btn btn-cancel"
-
diff --git a/app/views/projects/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml
index 01c810aee18..01fab3008a7 100644
--- a/app/views/projects/deploy_keys/new.html.haml
+++ b/app/views/projects/deploy_keys/new.html.haml
@@ -1,5 +1,5 @@
- page_title "New Deploy Key"
-%h3.page-title New Deploy key
+%h3.page-title New Deploy Key
%hr
= render 'form'
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 56b51f038ba..f9d661d59d2 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,9 +1,9 @@
-- if params[:view] == 'parallel'
+- if diff_view == 'parallel'
- fluid_layout true
- diff_files = safe_diff_files(diffs)
-.gray-content-block.second-block
+.gray-content-block.middle-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 410ff6abb43..517f6aef7c5 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -2,22 +2,29 @@
.diff-header{id: "file-path-#{hexdigest(diff_file.file_path)}"}
- if diff_file.diff.submodule?
%span
- - submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
- = submodule_link(submodule_item, @commit.id, project.repository)
+ = icon('archive fw')
+ %strong
+ = submodule_link(blob, @commit.id, project.repository)
- else
%span
+ = blob_icon blob.mode, blob.name
+ = link_to "#diff-#{i}" do
+ %strong
+ = diff_file.new_path
+
- if diff_file.deleted_file
- = "#{diff_file.old_path} deleted"
+ deleted
- elsif diff_file.renamed_file
- = "#{diff_file.old_path} renamed to #{diff_file.new_path}"
- - else
- = diff_file.new_path
+ renamed from
+ %strong
+ = diff_file.old_path
- if diff_file.mode_changed?
- %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
+ %small
+ = "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
.diff-controls
- - if blob.text?
+ - if blob_text_viewable?(blob)
= link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do
%i.fa.fa-comments
&nbsp;
@@ -25,15 +32,16 @@
- if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
- after: '&nbsp;', from_merge_request_id: @merge_request.id)
+ from_merge_request_id: @merge_request.id)
+ &nbsp;
= view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- - if blob.text?
- - if params[:view] == 'parallel'
+ - if blob_text_viewable?(blob)
+ - if diff_view == 'parallel'
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob, index: i
- else
= render "projects/diffs/text_file", diff_file: diff_file, index: i
@@ -42,4 +50,3 @@
= render "projects/diffs/image", diff_file: diff_file, old_file: old_file, file: blob, index: i
- else
.nothing-here-block No preview for this file type
-
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 8e49299223c..ef758b5fb7a 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -14,7 +14,7 @@
= f.label :name, class: 'control-label' do
Project name
.col-sm-10
- = f.text_field :name, placeholder: "Example Project", class: "form-control", id: "project_name_edit"
+ = f.text_field :name, class: "form-control", id: "project_name_edit"
.form-group
@@ -22,12 +22,12 @@
Project description
%span.light (optional)
.col-sm-10
- = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250
+ = f.text_area :description, class: "form-control", rows: 3, maxlength: 250
- - if @project.repository.exists? && @project.repository.branch_names.any?
+ - unless @project.empty_repo?
.form-group
= f.label :default_branch, "Default Branch", class: 'control-label'
- .col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'})
+ .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
@@ -35,7 +35,7 @@
.form-group
= f.label :tag_list, "Tags", class: 'control-label'
.col-sm-10
- = f.text_field :tag_list, maxlength: 2000, class: "form-control"
+ = 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
@@ -57,7 +57,16 @@
= f.check_box :merge_requests_enabled
%strong Merge Requests
%br
- %span.descr Submit changes to be merged upstream.
+ %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
@@ -103,6 +112,62 @@
%hr
= link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
+ %fieldset.features
+ %legend
+ Continuous Integration
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ %p Get recent application code using the following command:
+ .radio
+ = f.label :build_allow_git_fetch do
+ = f.radio_button :build_allow_git_fetch, 'false'
+ %strong git clone
+ %br
+ %span.descr Slower but makes sure you have a clean dir before every build
+ .radio
+ = f.label :build_allow_git_fetch do
+ = f.radio_button :build_allow_git_fetch, 'true'
+ %strong git fetch
+ %br
+ %span.descr Faster
+ .form-group
+ = f.label :build_timeout_in_minutes, 'Timeout', class: 'control-label'
+ .col-sm-10
+ = f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
+ %p.help-block per build in minutes
+ .form-group
+ = f.label :build_coverage_regex, "Test coverage parsing", class: 'control-label'
+ .col-sm-10
+ .input-group
+ %span.input-group-addon /
+ = f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
+ %span.input-group-addon /
+ %p.help-block
+ We will use this regular expression to find test coverage output in build trace.
+ Leave blank if you want to disable this feature
+ .bs-callout.bs-callout-info
+ %p Below are examples of regex for existing tools:
+ %ul
+ %li
+ Simplecov (Ruby) -
+ %code \(\d+.\d+\%\) covered
+ %li
+ pytest-cov (Python) -
+ %code \d+\%\s*$
+ %li
+ phpunit --coverage-text --colors=never (PHP) -
+ %code ^\s*Lines:\s*\d+.\d+\%
+
+
+ %fieldset.features
+ %legend
+ Advanced settings
+ .form-group
+ = f.label :runners_token, "CI token", class: 'control-label'
+ .col-sm-10
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used to checkout project.
+
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
@@ -121,9 +186,11 @@
The project can be committed to.
%br
%strong Once active this project shows up in the search and on the dashboard.
- = link_to 'Unarchive', 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"
+
+ .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
@@ -135,9 +202,11 @@
It is hidden from the dashboard and doesn't show up in searches.
%br
%strong Archived projects cannot be committed to!
- = link_to 'Archive', 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"
+
+ .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
@@ -162,7 +231,7 @@
Project name
.col-sm-9
.form-group
- = f.text_field :name, placeholder: "Example Project", class: "form-control"
+ = f.text_field :name, class: "form-control"
.form-group
= f.label :path, class: 'control-label' do
%span Path
@@ -172,12 +241,11 @@
.input-group-addon
#{URI.join(root_url, @project.namespace.path)}/
= f.text_field :path, class: 'form-control'
- %span.input-group-addon .git
%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', class: "btn btn-warning"
+ = f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project)
.panel.panel-default.panel.panel-danger
@@ -196,7 +264,7 @@
%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.
.form-actions
- = f.submit 'Transfer', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => transfer_project_message(@project) }
+ = 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
@@ -211,7 +279,8 @@
#{link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)}.
%br
%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) }
+ .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.
@@ -224,8 +293,8 @@
Removing the project will delete its repository and all related resources including issues, merge requests etc.
%br
%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) }
+ .form-actions
+ = button_to 'Remove project', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
- else
.nothing-here-block Only the project owner can remove a project.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index e06454fd148..503d156661e 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,54 +1,56 @@
-.alert_holder
+= content_for :flash_message do
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
= render 'shared/no_password'
-
+
= render "home_panel"
-.gray-content-block.center
+.gray-content-block.second-block.center
%h3.page-title
The repository for this project is empty
- %p
- If you already have files you can push them using command line instructions below.
- %br
- Otherwise you can start with
- = link_to "adding README", new_readme_path, class: 'underlined-link'
- file to this project.
+ - if can?(current_user, :push_code, @project)
+ %p
+ If you already have files you can push them using command line instructions below.
+ %p
+ Otherwise you can start with
+ = link_to "adding README", new_readme_path, class: 'underlined-link'
+ file to this project.
-.prepend-top-20
-.empty_wrapper
- %h3.page-title-empty
- Command line instructions
- %div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+- if can?(current_user, :download_code, @project)
+ .prepend-top-20
+ .empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index f0b0a11c04a..8a2c027a455 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -43,4 +43,3 @@
%i.fa.fa-spinner.fa-spin
Forking repository
%p Please wait a moment, this page will automatically refresh when ready.
-
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index bbfaf422a82..a47643bd09c 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,9 +1,11 @@
-%ul.nav.nav-tabs
+%ul.center-top-menu
= 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
- - if @project.gitlab_ci?
+ = 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/ci.html.haml b/app/views/projects/graphs/ci.html.haml
index 4f69cc64f7c..6fa77cc10c6 100644
--- a/app/views/projects/graphs/ci.html.haml
+++ b/app/views/projects/graphs/ci.html.haml
@@ -1,7 +1,16 @@
- page_title "Continuous Integration", "Graphs"
= render "header_title"
= render 'head'
+.gray-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'
+
+ %hr
= render 'projects/graphs/ci/builds'
- = render 'projects/graphs/ci/build_times'
-= render 'projects/graphs/ci/overall'
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
index c3c2f572414..c58223fd39e 100644
--- a/app/views/projects/graphs/ci/_build_times.haml
+++ b/app/views/projects/graphs/ci/_build_times.haml
@@ -1,21 +1,22 @@
-%fieldset
- %legend
+%div
+ %p.light
Commit duration in minutes for last 30 commits
- %canvas#build_timesChart.padded{width: 800, height: 300}
+ %canvas#build_timesChart{height: 200}
:javascript
var data = {
labels : #{@charts[:build_times].labels.to_json},
datasets : [
{
- fillColor : "#4A3",
- strokeColor : "rgba(151,187,205,1)",
- pointColor : "rgba(151,187,205,1)",
- pointStrokeColor : "#fff",
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
data : #{@charts[:build_times].build_times.to_json}
}
]
}
var ctx = $("#build_timesChart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true});
+ new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false});
diff --git a/app/views/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
index 1b0039fb834..8fca07114fa 100644
--- a/app/views/projects/graphs/ci/_builds.haml
+++ b/app/views/projects/graphs/ci/_builds.haml
@@ -1,20 +1,30 @@
-%fieldset
- %legend
- Builds chart for last week
- (#{date_from_to(Date.today - 7.days, Date.today)})
+%h4 Build charts
+%p
+ &nbsp;
+ %span.cgreen
+ = icon("circle")
+ success
+ &nbsp;
+ %span.cgray
+ = icon("circle")
+ all
- %canvas#weekChart.padded{width: 800, height: 200}
+.prepend-top-default
+ %p.light
+ Builds for last week
+ (#{date_from_to(Date.today - 7.days, Date.today)})
+ %canvas#weekChart{height: 200}
-%fieldset
- %legend
- Builds chart for last month
+.prepend-top-default
+ %p.light
+ Builds for last month
(#{date_from_to(Date.today - 30.days, Date.today)})
+ %canvas#monthChart{height: 200}
- %canvas#monthChart.padded{width: 800, height: 300}
-
-%fieldset
- %legend Builds chart for last year
- %canvas#yearChart.padded{width: 800, height: 400}
+.prepend-top-default
+ %p.light
+ Builds for last year
+ %canvas#yearChart.padded{height: 250}
- [:week, :month, :year].each do |scope|
:javascript
@@ -22,20 +32,20 @@
labels : #{@charts[scope].labels.to_json},
datasets : [
{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- pointColor : "rgba(220,220,220,1)",
+ fillColor : "#7f8fa4",
+ strokeColor : "#7f8fa4",
+ pointColor : "#7f8fa4",
pointStrokeColor : "#EEE",
data : #{@charts[scope].total.to_json}
},
{
- fillColor : "#4A3",
- strokeColor : "rgba(151,187,205,1)",
- pointColor : "rgba(151,187,205,1)",
+ fillColor : "#44aa22",
+ strokeColor : "#44aa22",
+ pointColor : "#44aa22",
pointStrokeColor : "#fff",
data : #{@charts[scope].success.to_json}
}
]
}
var ctx = $("##{scope}Chart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true});
+ new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, maintainAspectRatio: false});
diff --git a/app/views/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml
index 9550d719471..4b12e5f2da1 100644
--- a/app/views/projects/graphs/ci/_overall.haml
+++ b/app/views/projects/graphs/ci/_overall.haml
@@ -1,22 +1,19 @@
-- ci_project = @project.gitlab_ci_project
-%fieldset
- %legend Overall
- %p
+%h4 Overall stats
+%ul
+ %li
Total:
- %strong= pluralize ci_project.builds.count(:all), 'build'
- %p
+ %strong= pluralize @project.builds.count(:all), 'build'
+ %li
Successful:
- %strong= pluralize ci_project.builds.success.count(:all), 'build'
- %p
+ %strong= pluralize @project.builds.success.count(:all), 'build'
+ %li
Failed:
- %strong= pluralize ci_project.builds.failed.count(:all), 'build'
-
- %p
+ %strong= pluralize @project.builds.failed.count(:all), 'build'
+ %li
Success ratio:
%strong
- #{success_ratio(ci_project.builds.success, ci_project.builds.failed)}%
-
- %p
+ #{success_ratio(@project.builds.success, @project.builds.failed)}%
+ %li
Commits covered:
%strong
- = ci_project.commits.count(:all)
+ = @project.ci_commits.count(:all)
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index 112be875b6b..fc465ab273b 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,9 +1,13 @@
- page_title "Commits", "Graphs"
= render "header_title"
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
= render 'head'
+.gray-content-block.append-bottom-default
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs_commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
%p.lead
Commit statistics for
%strong #{@ref}
@@ -45,26 +49,24 @@
Commits per weekday
%canvas#weekday-chart
-:coffeescript
- responsiveChart = (selector, data) ->
- options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }
-
- # get selector by context
- ctx = selector.get(0).getContext("2d")
- # pointing parent container to make chart.js inherit its width
- container = $(selector).parent()
-
- generateChart = ->
- selector.attr('width', $(container).width())
- new Chart(ctx).Bar(data, options)
-
- # enabling auto-resizing
- $(window).resize( generateChart )
-
- generateChart()
+:javascript
+ var responsiveChart = function (selector, data) {
+ var options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false };
+ // get selector by context
+ var ctx = selector.get(0).getContext("2d");
+ // pointing parent container to make chart.js inherit its width
+ var container = $(selector).parent();
+ var generateChart = function() {
+ selector.attr('width', $(container).width());
+ return new Chart(ctx).Bar(data, options);
+ };
+ // enabling auto-resizing
+ $(window).resize(generateChart);
+ return generateChart();
+ };
- chartData = (keys, values) ->
- data = {
+ var chartData = function (keys, values) {
+ var data = {
labels : keys,
datasets : [{
fillColor : "rgba(220,220,220,0.5)",
@@ -74,13 +76,15 @@
barDatasetSpacing: 1,
data : values
}]
- }
+ };
+ return data;
+ };
- hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json})
- responsiveChart($('#hour-chart'), hourData)
+ var hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json});
+ responsiveChart($('#hour-chart'), hourData);
- dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json})
- responsiveChart($('#weekday-chart'), dayData)
+ var dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json});
+ responsiveChart($('#weekday-chart'), dayData);
- monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json})
- responsiveChart($('#month-chart'), monthData)
+ var monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json});
+ responsiveChart($('#month-chart'), monthData);
diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml
new file mode 100644
index 00000000000..a7fab5b6d72
--- /dev/null
+++ b/app/views/projects/graphs/languages.html.haml
@@ -0,0 +1,32 @@
+- page_title "Languages", "Graphs"
+= render "header_title"
+= render 'head'
+
+.gray-content-block.append-bottom-default
+ .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]
+ \%
+
+:javascript
+ var data = #{@languages.to_json};
+ var ctx = $("#languages-chart").get(0).getContext("2d");
+ var options = {
+ scaleOverlay: true,
+ responsive: true,
+ maintainAspectRatio: false
+ }
+ var myPieChart = new Chart(ctx).Pie(data, options);
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index bd342911e49..882e7d6b6ee 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,9 +1,13 @@
- page_title "Contributors", "Graphs"
= render "header_title"
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs'
= render 'head'
+.gray-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
@@ -24,18 +28,21 @@
-:coffeescript
- $.ajax
+:javascript
+ $.ajax({
type: "GET",
url: location.href,
- success: (data) ->
- graph = new ContributorsStatGraph()
- graph.init(data)
+ dataType: "json",
+ success: function (data) {
+ var graph = new ContributorsStatGraph();
+ graph.init(data);
- $("#brush_change").change ->
- graph.change_date_header()
- graph.redraw_authors()
+ $("#brush_change").change(function(){
+ graph.change_date_header();
+ graph.redraw_authors();
+ });
$(".stat-graph").fadeIn();
$(".loading-graph").hide();
- dataType: "json"
+ }
+ });
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index 85dbfd67862..b18d9197d0b 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -19,7 +19,7 @@
= f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
.form-group
= f.label :url, "Trigger", class: 'control-label'
- .col-sm-10
+ .col-sm-10.prepend-top-10
%div
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
@@ -55,6 +55,13 @@
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created
+ %div
+ = 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
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
@@ -78,7 +85,7 @@
.clearfix
%span.monospace= hook.url
%p
- - %w(push_events tag_push_events issues_events note_events merge_requests_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index 92a87690c54..6027fb23360 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -1,22 +1,19 @@
- page_title "Import repository"
%h3.page-title
- - if @project.import_failed?
- Import failed. Retry?
- - else
- Import repository
+ Import repository
%hr
+- if @project.import_failed?
+ .panel.panel-danger
+ .panel-heading The repository could not be imported.
+ .panel-body
+ %pre
+ :preserve
+ #{@project.import_error.try(:strip)}
+
= form_for @project, url: namespace_project_import_path(@project.namespace, @project), method: :post, html: { class: 'form-horizontal' } do |f|
- .form-group.import-url-data
- = f.label :import_url, class: 'control-label' do
- %span Import existing git repo
- .col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git'
- .well.prepend-top-20
- This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git.
- %br
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
- For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}
+ = render "shared/import_form", f: f
+
.form-actions
= f.submit 'Start import', class: "btn btn-create", tabindex: 4
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 06886d215a3..c0d1ce0d120 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -8,7 +8,7 @@
- else
Import in progress.
- unless @project.forked?
- %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
+ %p.monospace git clone --bare #{@project.safe_import_url}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
new ProjectImport();
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
index aef352029d0..de415ae51a4 100644
--- a/app/views/projects/issues/_closed_by_box.html.haml
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -1,3 +1,2 @@
-.issue-closed-by-widget
- = icon('check')
- This issue will be closed automatically when merge request #{gfm(merge_requests_sentence(@closed_by_merge_requests.sort))} is accepted.
+.issue-closed-by-widget.gray-content-block.second-block.white
+ This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index d4a98eca473..dc434cf38c4 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,36 +1,9 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- if @issue.closed?
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
+ = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- else
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue'
+ = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue'
-= render 'shared/show_aside'
-
-.gray-content-block.second-block
- .row
- .col-md-9
- .votes-holder.pull-right
- #votes= render 'votes/votes_block', votable: @issue
- .participants
- %span= pluralize(@participants.count, 'participant')
- - @participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
- .col-md-3
- %span.slead.has_tooltip{title: 'Cross-project reference'}
- = cross_project_reference(@project, @issue)
-
-.row
- %section.col-md-9
- .voting_notes#notes= render 'projects/notes/notes_with_form'
- %aside.col-md-3
- .issuable-affix
- .context
- = render 'shared/issuable/context', issuable: @issue
-
- - if @issue.labels.any?
- .issuable-context-title
- %label Labels
- .issue-show-labels
- - @issue.labels.each do |label|
- = link_to_label(label)
+#notes
+ = render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_form.html.haml b/app/views/projects/issues/_form.html.haml
index f39bb7d2574..6588d9bdbe1 100644
--- a/app/views/projects/issues/_form.html.haml
+++ b/app/views/projects/issues/_form.html.haml
@@ -1,9 +1,5 @@
-%div.issue-form-holder
- %h3.page-title= @issue.new_record? ? "Create Issue" : "Edit Issue ##{@issue.iid}"
- %hr
-
- = form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form' } do |f|
- = render 'shared/issuable/form', f: f, issuable: @issue
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], html: { class: 'form-horizontal issue-form gfm-form js-requires-input' } do |f|
+ = render 'shared/issuable/form', f: f, issuable: @issue
:javascript
$('.assign-to-me-link').on('click', function(e){
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 55ce912829d..f9cf4910df3 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -6,37 +6,42 @@
.issue-title
%span.issue-title-text
= link_to_gfm issue.title, issue_path(issue), class: "row_title"
- .issue-labels
- - issue.labels.each do |label|
- = link_to_label(label, project: issue.project)
- .pull-right.light
+ %ul.controls.light
- if issue.closed?
- %span
+ %li
CLOSED
+
- if issue.assignee
- = link_to_member(@project, issue.assignee, name: false)
+ %li
+ = link_to_member(@project, issue.assignee, name: false, title: "Assigned to :name")
+
- note_count = issue.notes.user.count
- if note_count > 0
- &nbsp;
- %span
- %i.fa.fa-comments
- = note_count
+ %li
+ = link_to issue_path(issue) + "#notes" do
+ = icon('comments')
+ = note_count
- else
- &nbsp;
- %span.issue-no-comments
- %i.fa.fa-comments
- = 0
+ %li
+ = link_to issue_path(issue) + "#notes", class: "issue-no-comments" do
+ = icon('comments')
+ = note_count
.issue-info
- = "#{issue.to_reference} opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
- - if issue.votes_count > 0
- = render 'votes/votes_inline', votable: issue
+ #{issue.to_reference} &middot;
+ opened #{time_ago_with_tooltip(issue.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, issue.author, avatar: false)}
- if issue.milestone
&nbsp;
- %span
- %i.fa.fa-clock-o
+ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
+ = icon('clock-o')
= issue.milestone.title
+ - if issue.labels.any?
+ &nbsp;
+ - issue.labels.each do |label|
+ = link_to_label(label, project: issue.project)
- if issue.tasks?
+ &nbsp;
%span.task-status
= issue.task_status
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index a3399c57aa2..e0e89b764d5 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -5,8 +5,9 @@
.nothing-here-block No issues to show
- if @issues.present?
- .pull-right
- %span.issue_counter #{@issues.total_count}
- issues for this filter
+ .issuable-filter-count
+ %span.pull-right
+ = number_with_delimiter(@issues.total_count)
+ issues for this filter
= paginate @issues, theme: "gitlab"
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
new file mode 100644
index 00000000000..254968e4f67
--- /dev/null
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -0,0 +1,26 @@
+-if @merge_requests.any?
+ %h2.merge-requests-title
+ = pluralize(@merge_requests.count, 'Related Merge Request')
+ %ul.bordered-list
+ - has_any_ci = @merge_requests.any?(&:ci_commit)
+ - @merge_requests.each do |merge_request|
+ %li
+ %span.merge-request-ci-status
+ - if merge_request.ci_commit
+ = render_ci_status(merge_request.ci_commit)
+ - elsif has_any_ci
+ = icon('blank fw')
+ %span.merge-request-id
+ \##{merge_request.iid}
+ %span.merge-request-info
+ %strong
+ = link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
+ - unless @issue.project.id == merge_request.target_project.id
+ in
+ - project = merge_request.target_project
+ = link_to project.name_with_namespace, namespace_project_path(project.namespace, project)
+ %span.merge-request-status.prepend-left-10
+ - if merge_request.merged?
+ MERGED
+ - elsif merge_request.closed?
+ CLOSED
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 53b6f0879c9..20216297d25 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,2 +1,8 @@
- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
+= render "header_title"
+
+%h3.page-title
+ Edit Issue ##{@issue.iid}
+%hr
+
= render "form"
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index 153447baa1b..b317a0c1cf4 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,4 +1,8 @@
- page_title "New Issue"
= render "header_title"
+%h3.page-title
+ New Issue
+%hr
+
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f01bf2505da..f931a0d3b92 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,52 +1,67 @@
-- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_description @issue.description
+- page_card_attributes @issue.card_attributes
+
= render "header_title"
.issue
+ .detail-page-header
+ .status-box{ class: "status-box-closed #{issue_button_visibility(@issue, false)}"} Closed
+ .status-box{ class: "status-box-open #{issue_button_visibility(@issue, true)}"} Open
+ %span.identifier
+ Issue ##{@issue.iid}
+ %span.creator
+ &middot;
+ opened by #{link_to_member(@project, @issue.author, size: 24)}
+ &middot;
+ = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
+ - if @issue.updated_at != @issue.created_at
+ %span
+ &middot;
+ = icon('edit', title: 'edited')
+ = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
+
+ .pull-right
+ - if can?(current_user, :create_issue, @project)
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-nr btn-grouped new-issue-link btn-success', title: 'New Issue', id: 'new_issue_link' do
+ = icon('plus')
+ New Issue
+ - if can?(current_user, :update_issue, @issue)
+ = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
+ = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
+
+ = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-nr btn-grouped issuable-edit' do
+ = icon('pencil-square-o')
+ Edit
+
.issue-details.issuable-details
- .page-title
- .issue-box{ class: issue_box_class(@issue) }
- - if @issue.closed?
- Closed
- - else
- Open
- %span.issue-id Issue ##{@issue.iid}
- %span.creator
- &middot; created by #{link_to_member(@project, @issue.author, size: 24)}
- &middot;
- = time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
- - if @issue.updated_at != @issue.created_at
- %span
- &middot;
- = icon('edit', title: 'edited')
- = time_ago_with_tooltip(@issue.updated_at, placement: 'bottom', html_class: 'issue_edited_ago')
-
- .pull-right
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do
- = icon('plus')
- New Issue
- - if can?(current_user, :update_issue, @issue)
- - if @issue.closed?
- = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen'
- - else
- = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue'
-
- = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do
- = icon('pencil-square-o')
- Edit
-
- .gray-content-block.middle-block
- %h2.issue-title
- = gfm escape_once(@issue.title)
+ .detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(@issue.title), pipeline: :single_line
%div
- if @issue.description.present?
.description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
.wiki
= preserve do
- = markdown(@issue.description)
+ = markdown(@issue.description, cache_key: [@issue, "description"])
%textarea.hidden.js-task-list-field
= @issue.description
- - if @closed_by_merge_requests.present?
- = render 'projects/issues/closed_by_box'
- .issue-discussion
- = render 'projects/issues/discussion'
+
+ .merge-requests
+ = render 'merge_requests'
+
+ .gray-content-block.second-block.oneline-block
+ = render 'votes/votes_block', votable: @issue
+
+ - if @closed_by_merge_requests.present?
+ = render 'projects/issues/closed_by_box'
+
+ .row
+ %section.col-md-9
+ .issuable-discussion
+ = render 'projects/issues/discussion'
+
+ %aside.col-md-3
+ = render 'shared/issuable/sidebar', issuable: @issue
+
+ = render 'shared/show_aside'
diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml
index b7735aaf3c1..2f0f3fcfb06 100644
--- a/app/views/projects/issues/update.js.haml
+++ b/app/views/projects/issues/update.js.haml
@@ -1,3 +1,3 @@
-$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @issue)}");
-$('.context').effect('highlight')
+$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @issue)}");
+$('.issuable-sidebar').parent().effect('highlight')
new Issue();
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index 4cf13492e99..5ce2a7b985d 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -10,9 +10,9 @@
.form-group
= f.label :title, class: 'control-label'
.col-sm-10
- = f.text_field :title, class: "form-control js-quick-submit", required: true
+ = f.text_field :title, class: "form-control js-quick-submit", required: true, autofocus: true
.form-group
- = f.label :color, "Background Color", class: 'control-label'
+ = f.label :color, "Background color", class: 'control-label'
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
@@ -28,6 +28,8 @@
&nbsp;
.form-actions
- = f.submit 'Save', class: 'btn btn-save js-save-button'
+ - if @label.persisted?
+ = f.submit 'Save changes', class: 'btn btn-save js-save-button'
+ - else
+ = f.submit 'Create Label', class: 'btn btn-create js-save-button'
= link_to "Cancel", namespace_project_labels_path(@project.namespace, @project), class: 'btn btn-cancel'
-
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index c6ebfa281a1..b70a9fc9fe5 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -7,4 +7,4 @@
- if can? current_user, :admin_label, @project
= link_to 'Edit', edit_namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm'
- = link_to 'Remove', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
+ = link_to 'Delete', namespace_project_label_path(@project.namespace, @project, label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
diff --git a/app/views/projects/labels/destroy.js.haml b/app/views/projects/labels/destroy.js.haml
index 1b4c83ab097..d59563b122a 100644
--- a/app/views/projects/labels/destroy.js.haml
+++ b/app/views/projects/labels/destroy.js.haml
@@ -1,2 +1,2 @@
- if @project.labels.size == 0
- $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000)
+ $('.labels').load(document.URL + ' .nothing-here-block').hide().fadeIn(1000)
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index bc4ab0ca27c..675a805e12f 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,11 +1,7 @@
- page_title "Edit", @label.name, "Labels"
= render "header_title"
-%h3
- Edit label
- %span.light #{@label.name}
-.back-link
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- &larr; To labels list
+%h3.page-title
+ Edit Label
%hr
= render 'form'
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 97175f8232b..9081bcfe9b3 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -4,6 +4,7 @@
.gray-content-block.top-block
- if can? current_user, :admin_label, @project
= link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
+ = icon('plus')
New label
.oneline
Labels can be applied to issues and merge requests.
@@ -14,8 +15,8 @@
= render @labels
= paginate @labels, theme: 'gitlab'
- else
- .light-well
+ .nothing-here-block
- if can? current_user, :admin_label, @project
- .nothing-here-block Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels
+ Create first label or #{link_to 'generate', generate_namespace_project_labels_path(@project.namespace, @project), method: :post} default set of labels
- else
- .nothing-here-block No labels created
+ No labels created
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 342ad4f3f95..e20fd7d6891 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,9 +1,7 @@
- page_title "New Label"
= render "header_title"
-%h3 New label
-.back-link
- = link_to namespace_project_labels_path(@project.namespace, @project) do
- &larr; To labels list
+%h3.page-title
+ New Label
%hr
= render 'form'
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 38e66c3828b..bff3c3b283d 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,33 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
- if @merge_request.closed?
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
-= render 'shared/show_aside'
-
-.gray-content-block.second-block
- .row
- .col-md-9
- .votes-holder.pull-right
- #votes= render 'votes/votes_block', votable: @merge_request
- = render "projects/merge_requests/show/participants"
- .col-md-3
- %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
- = cross_project_reference(@project, @merge_request)
-
-.row
- %section.col-md-9
- = render "projects/notes/notes_with_form"
- %aside.col-md-3
- .issuable-affix
- .context
- = render 'shared/issuable/context', issuable: @merge_request
-
- - if @merge_request.labels.any?
- .issuable-context-title
- %label Labels
- .merge-request-show-labels
- - @merge_request.labels.each do |label|
- = link_to_label(label)
+#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml
index 9cf389dbe38..3e4ab09c6d4 100644
--- a/app/views/projects/merge_requests/_form.html.haml
+++ b/app/views/projects/merge_requests/_form.html.haml
@@ -1,6 +1,5 @@
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f|
- .merge-request-form-info
- = render 'shared/issuable/form', f: f, issuable: @merge_request
+ = render 'shared/issuable/form', f: f, issuable: @merge_request
:javascript
$('.assign-to-me-link').on('click', function(e){
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 25e4e8ba80d..a051729dc32 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -2,43 +2,60 @@
.merge-request-title
%span.merge-request-title-text
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
- .merge-request-labels
- - merge_request.labels.each do |label|
- = link_to_label(label, project: merge_request.project)
- .pull-right.light
+ %ul.controls.light
- if merge_request.merged?
- %span
- %i.fa.fa-check
+ %li
MERGED
- elsif merge_request.closed?
- %span
- %i.fa.fa-ban
+ %li
+ = icon('ban')
CLOSED
- - note_count = merge_request.mr_and_commit_notes.user.count
+
+ - if merge_request.ci_commit
+ %li
+ = render_ci_status(merge_request.ci_commit)
+
+ - if merge_request.open? && merge_request.broken?
+ %li
+ = link_to merge_request_path(merge_request), class: "has_tooltip", title: "Cannot be merged automatically", data: { container: 'body' } do
+ = icon('exclamation-triangle')
+
- if merge_request.assignee
- &nbsp;
- = link_to_member(merge_request.source_project, merge_request.assignee, name: false)
+ %li
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false, title: "Assigned to :name")
+
+ - note_count = merge_request.mr_and_commit_notes.user.count
- if note_count > 0
- &nbsp;
- %span
- %i.fa.fa-comments
- = note_count
+ %li
+ = link_to merge_request_path(merge_request) + "#notes" do
+ = icon('comments')
+ = note_count
- else
- &nbsp;
- %span.merge-request-no-comments
- %i.fa.fa-comments
- = 0
+ %li
+ = link_to merge_request_path(merge_request) + "#notes", class: "merge-request-no-comments" do
+ = icon('comments')
+ = note_count
.merge-request-info
- = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
- - if merge_request.votes_count > 0
- = render 'votes/votes_inline', votable: merge_request
- - if merge_request.milestone_id?
+ \##{merge_request.iid} &middot;
+ opened #{time_ago_with_tooltip(merge_request.created_at, placement: 'bottom')}
+ by #{link_to_member(@project, merge_request.author, avatar: false)}
+ - if merge_request.target_project.default_branch != merge_request.target_branch
+ &nbsp;
+ = link_to namespace_project_commits_path(merge_request.project.namespace, merge_request.project, merge_request.target_branch) do
+ = icon('code-fork')
+ = merge_request.target_branch
+ - if merge_request.milestone
&nbsp;
- %span
- %i.fa.fa-clock-o
+ = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, milestone_title: merge_request.milestone.title) do
+ = icon('clock-o')
= merge_request.milestone.title
+ - if merge_request.labels.any?
+ &nbsp;
+ - merge_request.labels.each do |label|
+ = link_to_label(label, project: merge_request.project)
- if merge_request.tasks?
+ &nbsp;
%span.task-status
= merge_request.task_status
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index d86707b3d97..29d09d0a652 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -5,8 +5,10 @@
.nothing-here-block No merge requests to show
- if @merge_requests.present?
- .pull-right
- %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter
+ .issuable-filter-count
+ %span.pull-right
+ = number_with_delimiter(@merge_requests.total_count)
+ merge requests for this filter
= paginate @merge_requests, theme: "gitlab"
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 452006162db..236a545c840 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -1,4 +1,5 @@
-%p.lead Compare branches for new Merge Request
+%h3.page-title
+ New Merge Request
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline js-requires-input" } do |f|
.hide.alert.alert-danger.mr-compare-errors
@@ -10,7 +11,7 @@
.panel-body
= f.select(:source_project_id, [[@merge_request.source_project_path,@merge_request.source_project.id]] , {}, { class: 'source_project select2 span3', disabled: @merge_request.persisted?, required: true })
&nbsp;
- = f.select(:source_branch, @merge_request.source_branches, { include_blank: "Select branch" }, {class: 'source_branch select2 span2', required: true})
+ = f.select(:source_branch, @merge_request.source_branches, { include_blank: true }, { class: 'source_branch select2 span2', required: true, data: { placeholder: "Select source branch" } })
.panel-footer
.mr_source_commit
@@ -22,7 +23,7 @@
- projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project]
= f.select(:target_project_id, options_from_collection_for_select(projects, 'id', 'path_with_namespace', f.object.target_project_id), {}, { class: 'target_project select2 span3', disabled: @merge_request.persisted?, required: true })
&nbsp;
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, {class: 'target_branch select2 span2', required: true})
+ = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', required: true, data: { placeholder: "Select target branch" } })
.panel-footer
.mr_target_commit
@@ -37,7 +38,7 @@
%h4 Compare failed
%p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- else
- .light-well.append-bottom-10
+ .light-well.append-bottom-default
.center
%h4
There isn't anything to merge.
@@ -51,8 +52,8 @@
are the same.
- %div
- = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn"
+ .form-actions
+ = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
var source_branch = $("#merge_request_source_branch")
@@ -77,12 +78,12 @@
});
-:coffeescript
-
- $(".merge-request-form").on 'submit', ->
- if $("#merge_request_source_branch").val() is "" or $('#merge_request_target_branch').val() is ""
- $(".mr-compare-errors").html("You must select source and target branch to proceed")
- $(".mr-compare-errors").fadeIn()
- event.preventDefault()
- return
-
+:javascript
+ $(".merge-request-form").on('submit', function () {
+ if ($("#merge_request_source_branch").val() === "" || $('#merge_request_target_branch').val() === "") {
+ $(".mr-compare-errors").html("You must select source and target branch to proceed");
+ $(".mr-compare-errors").fadeIn();
+ event.preventDefault();
+ return;
+ }
+ });
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 6244d3ba0b4..a14943b15d3 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -1,5 +1,5 @@
%h3.page-title
- New merge request
+ New Merge Request
%p.slead
- source_title, target_title = format_mr_branch_names(@merge_request)
From
@@ -11,27 +11,31 @@
= link_to 'Change branches', mr_change_branches_path(@merge_request)
%hr
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form js-requires-input' } do |f|
- .merge-request-form-info
- = render 'shared/issuable/form', f: f, issuable: @merge_request
- = f.hidden_field :source_project_id
- = f.hidden_field :source_branch
- = f.hidden_field :target_project_id
- = f.hidden_field :target_branch
+ = render 'shared/issuable/form', f: f, issuable: @merge_request
+ = f.hidden_field :source_project_id
+ = f.hidden_field :source_branch
+ = f.hidden_field :target_project_id
+ = f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs
+ %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
%li.commits-tab
- = link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do
+ = link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
%span.badge= @commits.size
+ - if @ci_commit
+ %li.builds-tab.active
+ = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
+ Builds
+ %span.badge= @statuses.size
%li.diffs-tab.active
- = link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do
+ = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
Changes
%span.badge= @diffs.size
.tab-content
#commits.commits.tab-pane
- = render "projects/commits/commits", project: @project
+ = render "projects/merge_requests/show/commits"
#diffs.diffs.tab-pane.active
- if @diffs.present?
= render "projects/diffs/diffs", diffs: @diffs, project: @project
@@ -43,6 +47,9 @@
.alert.alert-danger
%h4 This comparison includes a huge diff.
%p To preserve performance the line changes are not shown.
+ - if @ci_commit
+ #builds.builds.tab-pane
+ = render "projects/merge_requests/show/builds"
:javascript
$('.assign-to-me-link').on('click', function(e){
@@ -57,4 +64,3 @@
diffs_loaded: true,
commits_loaded: true
});
-
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index eeaa72ed21b..ba7c2c01e93 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,14 +1,18 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_description @merge_request.description
+- page_card_attributes @merge_request.card_attributes
+
= render "header_title"
- if params[:view] == 'parallel'
- fluid_layout true
.merge-request{'data-url' => merge_request_path(@merge_request)}
+ = render "projects/merge_requests/show/mr_title"
+
.merge-request-details.issuable-details
- = render "projects/merge_requests/show/mr_title"
= render "projects/merge_requests/show/mr_box"
- .append-bottom-20.mr-source-target.prepend-top-default
+ .append-bottom-default.mr-source-target.prepend-top-default
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
@@ -26,44 +30,62 @@
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
.normal
%span Request to merge
- %span.label-branch #{source_branch_with_namespace(@merge_request)}
+ %span.label-branch= source_branch_with_namespace(@merge_request)
%span into
- %span.label-branch #{@merge_request.target_branch}
+ = link_to namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch" do
+ = @merge_request.target_branch
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
- - if @merge_request.open? && @merge_request.can_be_merged?
- .light.append-bottom-20
+ - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user)
+ .light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- - if @commits.present?
- %ul.merge-request-tabs
- %li.notes-tab
- = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do
- Discussion
- %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: '#commits', action: 'commits', toggle: 'tab'} do
- Commits
- %span.badge= @commits.size
- %li.diffs-tab
- = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do
- Changes
- %span.badge= @merge_request.diffs.size
+ - if @commits.present?
+ %ul.merge-request-tabs.center-top-menu.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.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
+ %li.builds-tab
+ = link_to builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#builds', action: 'builds', toggle: 'tab'} do
+ Builds
+ %span.badge= @statuses.size
+ %li.diffs-tab
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
+ Changes
+ %span.badge= @merge_request.diffs.size
+
+ .tab-content
+ #notes.notes.tab-pane.voting_notes
+ .gray-content-block.second-block.oneline-block
+ = render 'votes/votes_block', votable: @merge_request
+
+ .row
+ %section.col-md-9
+ .issuable-discussion
+ = render "projects/merge_requests/discussion"
+ %aside.col-md-3
+ = render 'shared/issuable/sidebar', issuable: @merge_request
+ = render 'shared/show_aside'
- .tab-content
- #notes.notes.tab-pane.voting_notes
- = render "projects/merge_requests/discussion"
- #commits.commits.tab-pane
- - # This tab is always loaded via AJAX
- #diffs.diffs.tab-pane
- - # This tab is always loaded via AJAX
+ #commits.commits.tab-pane
+ - # This tab is always loaded via AJAX
+ #builds.builds.tab-pane
+ - # This tab is always loaded via AJAX
+ #diffs.diffs.tab-pane
+ - # This tab is always loaded via AJAX
- .mr-loading-status
- = spinner
+ .mr-loading-status
+ = spinner
:javascript
var merge_request;
diff --git a/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml
new file mode 100644
index 00000000000..eab5be488b5
--- /dev/null
+++ b/app/views/projects/merge_requests/cancel_merge_when_build_succeeds.js.haml
@@ -0,0 +1,2 @@
+:plain
+ $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}");
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 303ca0a880b..fc62bb5bce9 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -2,6 +2,6 @@
= render "header_title"
%h3.page-title
- = "Edit merge request ##{@merge_request.iid}"
+ Edit Merge Request ##{@merge_request.iid}
%hr
= render 'form'
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 086298e5af1..8d5d0394a82 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -6,9 +6,10 @@
.controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- - if can? current_user, :create_merge_request, @project
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
.pull-left.hidden-xs
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new", title: "New Merge Request" do
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do
%i.fa.fa-plus
New Merge Request
= render 'shared/issuable/filter', type: :merge_requests
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index 33321651e32..92ce479d463 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -1,6 +1,10 @@
-- if @status
+- case @status
+- when :success
:plain
- merge_request_widget.mergeInProgress();
+ merge_request_widget.mergeInProgress(#{params[:should_remove_source_branch] == '1'});
+- when :merge_when_build_succeeds
+ :plain
+ $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_build_succeeds'))}");
- 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 9fdde80c6d9..d259968030e 100644
--- a/app/views/projects/merge_requests/new.html.haml
+++ b/app/views/projects/merge_requests/new.html.haml
@@ -1,7 +1,7 @@
- page_title "New Merge Request"
= render "header_title"
-- if @merge_request.can_be_created
+- if @merge_request.can_be_created && !params[:change_branches]
= render 'new_submit'
- else
= render 'new_compare'
diff --git a/app/views/projects/merge_requests/show/_builds.html.haml b/app/views/projects/merge_requests/show/_builds.html.haml
new file mode 100644
index 00000000000..307a75d02ca
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_builds.html.haml
@@ -0,0 +1 @@
+= render "projects/commit/builds", 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 a71b181a6a5..7f904ec42a0 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1 +1,5 @@
+.gray-content-block.middle-block.oneline-block
+ = icon("sort-amount-desc")
+ Most recent commits displayed first
+
= render "projects/commits/commits", project: @merge_request.project
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 626970f39be..d9cfc3d7ae9 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,5 +1,5 @@
- if @merge_request_diff.collected?
- = render "projects/diffs/diffs", diffs: @merge_request.diffs, project: @merge_request.project
+ = render "projects/diffs/diffs", diffs: params[:w] == '1' ? @merge_request.diffs_no_whitespace : @merge_request.diffs, project: @merge_request.project
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index f18cf96c17d..877cc3d744b 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
@@ -3,12 +3,13 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3 Check out, review and merge locally
+ %h3 Check out, review, and merge locally
.modal-body
%p
- %strong Step 1.
+ %strong Step 1.
Fetch and check out the branch for this merge request
- %pre.dark
+ = clipboard_button(clipboard_target: 'pre#merge-info-1')
+ %pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch}
@@ -24,7 +25,8 @@
%p
%strong Step 3.
Merge the branch and fix any conflicts that come up
- %pre.dark
+ = clipboard_button(clipboard_target: 'pre#merge-info-3')
+ %pre.dark#merge-info-3
- if @merge_request.for_fork?
:preserve
git checkout #{h @merge_request.target_branch}
@@ -36,7 +38,8 @@
%p
%strong Step 4.
Push the result of the merge to GitLab
- %pre.dark
+ = clipboard_button(clipboard_target: 'pre#merge-info-4')
+ %pre.dark#merge-info-4
:preserve
git push origin #{h @merge_request.target_branch}
- unless @merge_request.can_be_merged_by?(current_user)
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 b4f62a75890..0f81e5e8914 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,12 +1,12 @@
-.gray-content-block.middle-block
- %h2.issue-title
- = gfm escape_once(@merge_request.title)
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(@merge_request.title), pipeline: :single_line
%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)
+ = markdown(@merge_request.description, cache_key: [@merge_request, "description"])
%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 2bf9cd597a4..fc6fb2a0d42 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,10 +1,11 @@
-.page-title
- .issue-box{ class: issue_box_class(@merge_request) }
+.detail-page-header
+ .status-box{ class: status_box_class(@merge_request) }
= @merge_request.state_human_name
- %span.issue-id Merge Request ##{@merge_request.iid}
+ %span.identifier
+ Merge Request ##{@merge_request.iid}
%span.creator
&middot;
- created by #{link_to_member(@project, @merge_request.author, size: 24)}
+ opened by #{link_to_member(@project, @merge_request.author, size: 24)}
&middot;
= time_ago_with_tooltip(@merge_request.created_at)
- if @merge_request.updated_at != @merge_request.created_at
@@ -16,9 +17,9 @@
.issue-btn-group.pull-right
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "btn btn-grouped btn-close", title: "Close merge request"
- = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "btn btn-grouped issuable-edit", id: "edit_merge_request" do
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: 'btn btn-nr btn-grouped btn-close', title: 'Close merge request'
+ = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn btn-nr btn-grouped issuable-edit', id: 'edit_merge_request' do
%i.fa.fa-pencil-square-o
Edit
- if @merge_request.closed?
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link", title: "Close merge request"
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'btn btn-nr btn-grouped btn-reopen reopen-mr-link', title: 'Reopen merge request'
diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml
deleted file mode 100644
index c67afe963e7..00000000000
--- a/app/views/projects/merge_requests/show/_participants.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-.participants
- %span #{@participants.count} participants
- - @participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml
index 25583b2cc6f..93db65ddf79 100644
--- a/app/views/projects/merge_requests/update.js.haml
+++ b/app/views/projects/merge_requests/update.js.haml
@@ -1,3 +1,3 @@
-$('.context').html("#{escape_javascript(render 'shared/issuable/context', issuable: @merge_request)}");
-$('.context').effect('highlight')
+$('.issuable-sidebar').html("#{escape_javascript(render 'shared/issuable/sidebar', issuable: @merge_request)}");
+$('.issuable-sidebar').parent().effect('highlight')
merge_request = new MergeRequest();
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 10efb811939..b05ab869215 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,30 +1,33 @@
-- ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha)
-- if ci_commit
- - status = ci_commit.status
+- if @ci_commit
.mr-widget-heading
- .ci_widget{class: "ci-#{status}"}
- = ci_status_icon(ci_commit)
- %span CI build #{status}
- for #{@merge_request.last_commit_short_sha}.
+ .ci_widget{class: "ci-#{@ci_commit.status}"}
+ = ci_status_icon(@ci_commit)
+ %span
+ Build
+ = ci_status_label(@ci_commit)
+ for
+ = 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"
%span.ci-coverage
- = link_to "View build details", ci_status_path(ci_commit)
+ = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
- # Remove in later versions when services like Jenkins will set CI status via Commit status API
.mr-widget-heading
- - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
+ - %w[success skipped canceled failed running pending].each do |status|
.ci_widget{class: "ci-#{status}", style: "display:none"}
- - if status == :success
- - status = "passed"
- = icon("check-circle")
- - else
- = icon("circle")
- %span CI build #{status}
- for #{@merge_request.last_commit_short_sha}.
+ = ci_icon_for_status(status)
+ %span
+ CI build
+ = ci_label_for_status(status)
+ for
+ - commit = @merge_request.last_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
- - if ci_build_details_path(@merge_request)
- = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
+ - if details_path = ci_build_details_path(@merge_request)
+ = link_to "View details", details_path, :"data-no-turbolink" => "data-no-turbolink"
.ci_widget
= icon("spinner spin")
@@ -38,6 +41,7 @@
= icon("times-circle")
Could not connect to the CI server. Please check your settings and try again.
- :coffeescript
- $ ->
- merge_request_widget.getCiStatus()
+ :javascript
+ $(function() {
+ merge_request_widget.getCiStatus();
+ });
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index f223f687def..d1d602eecdc 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -7,43 +7,42 @@
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?
- = succeed '.' do
- The changes were merged into
- %span.label-branch= @merge_request.target_branch
+ - if !@merge_request.source_branch_exists? || (params[:delete_source] == 'true')
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
The source branch has been removed.
- - elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch)
+ - elsif @merge_request.can_remove_source_branch?(current_user)
.remove_source_branch_widget
- %p
- = succeed '.' do
- The changes were merged into
- %span.label-branch= @merge_request.target_branch
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
You can remove the source branch now.
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
%i.fa.fa-times
Remove Source Branch
.remove_source_branch_widget.failed.hide
- %p
+ %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 reload.
-
- :coffeescript
- $('.remove_source_branch').on 'click', ->
- $('.remove_source_branch_widget').hide()
- $('.remove_source_branch_in_progress').show()
-
- $(".remove_source_branch").on "ajax:success", (e, data, status, xhr) ->
- location.reload()
-
- $(".remove_source_branch").on "ajax:error", (e, data, status, xhr) ->
- $('.remove_source_branch_widget').hide()
- $('.remove_source_branch_in_progress').hide()
- $('.remove_source_branch_widget.failed').show()
-
-
+ 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();
+ });
+
+ $(".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();
+ });
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index 0aad9bb3e88..55dbae598d3 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -13,6 +13,8 @@
= render 'projects/merge_requests/widget/open/conflicts'
- elsif @merge_request.work_in_progress?
= render 'projects/merge_requests/widget/open/wip'
+ - elsif @merge_request.merge_when_build_succeeds?
+ = 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.can_be_merged?
@@ -24,4 +26,4 @@
%i.fa.fa-check
Accepting this merge request will close #{"issue".pluralize(@closes_issues.size)}
= succeed '.' do
- != gfm(issues_sentence(@closes_issues))
+ != markdown issues_sentence(@closes_issues), pipeline: :gfm
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 613525437ab..d9a1730a8bc 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -1,25 +1,60 @@
+- status_class = @ci_commit ? " ci-#{@ci_commit.status}" : nil
+
= form_for [:merge, @project.namespace.becomes(Namespace), @project, @merge_request], remote: true, method: :post, html: { class: 'accept-mr-form js-requires-input' } do |f|
= hidden_field_tag :authenticity_token, form_authenticity_token
.accept-merge-holder.clearfix.js-toggle-container
- .accept-action
- = f.button class: "btn btn-create accept_merge_request" do
- Accept Merge Request
- - if can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && !@merge_request.for_fork?
- .accept-control.checkbox
- = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
- = check_box_tag :should_remove_source_branch
- Remove source branch
- .accept-control.right
- = link_to "#", class: "modify-merge-commit-link js-toggle-button" do
- = icon('edit')
- Modify commit message
- .js-toggle-content.hide.prepend-top-20
+ .clearfix
+ .accept-action
+ - if @ci_commit && @ci_commit.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
+ - 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)
+ .accept-control.checkbox
+ = label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
+ = check_box_tag :should_remove_source_branch
+ Remove source branch
+ .accept-control.right
+ = link_to "#", class: "modify-merge-commit-link js-toggle-button" do
+ = icon('edit')
+ Modify commit message
+ .js-toggle-content.hide.prepend-top-default
= render 'shared/commit_message_container', params: params,
text: @merge_request.merge_commit_message,
rows: 14, hint: true
- :coffeescript
- $('.accept-mr-form').on 'ajax:before', ->
- btn = $('.accept_merge_request')
- btn.disable()
- btn.html("<i class='fa fa-spinner fa-spin'></i> Merge in progress")
+ = hidden_field_tag :merge_when_build_succeeds, "", autocomplete: "off"
+
+ :javascript
+ $('.accept-mr-form').on('ajax:send', function() {
+ $(".accept-mr-form :input").disable();
+ });
+
+ $('.accept_merge_request').on('click', function() {
+ $('.js-merge-button').html("<i class='fa fa-spinner fa-spin'></i> Merge in progress");
+ });
+
+ $('.merge_when_build_succeeds').on('click', function() {
+ $("#merge_when_build_succeeds").val("1");
+ });
+
+ $('.js-merge-dropdown a').on('click', function(e) {
+ e.preventDefault();
+ $(this).closest("form").submit();
+ });
diff --git a/app/views/projects/merge_requests/widget/open/_check.html.haml b/app/views/projects/merge_requests/widget/open/_check.html.haml
index b6b8974297e..e16878ba513 100644
--- a/app/views/projects/merge_requests/widget/open/_check.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_check.html.haml
@@ -2,6 +2,8 @@
= icon("spinner spin")
Checking ability to merge automatically&hellip;
-:coffeescript
- $ ->
- merge_request_widget.getMergeStatus()
+:javascript
+ $(function() {
+ merge_request_widget.getMergeStatus();
+ });
+
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
new file mode 100644
index 00000000000..2168294c683
--- /dev/null
+++ b/app/views/projects/merge_requests/widget/open/_merge_when_build_succeeds.html.haml
@@ -0,0 +1,26 @@
+%h4
+ 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
+ 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
+ - 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
+ = icon('times')
+ Remove Source Branch When Merged
+
+ - if user_can_cancel_automatic_merge
+ = link_to cancel_merge_when_build_succeeds_namespace_project_merge_request_path(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request), remote: true, method: :post, class: "btn btn-grouped btn-warning btn-sm" do
+ Cancel Automatic Merge
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index 255ddab479f..39aa2437e18 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -1,10 +1,3 @@
-%h3.page-title= @milestone.new_record? ? "New Milestone" : "Edit Milestone ##{@milestone.iid}"
-.back-link
- = link_to namespace_project_milestones_path(@project.namespace, @project) do
- &larr; To milestones
-
-%hr
-
= form_for [@project.namespace.becomes(Namespace), @project, @milestone], html: {class: 'form-horizontal milestone-form gfm-form js-requires-input'} do |f|
-if @milestone.errors.any?
.alert.alert-danger
@@ -16,16 +9,13 @@
.form-group
= f.label :title, "Title", class: "control-label"
.col-sm-10
- = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true
- %p.hint Required
+ = f.text_field :title, maxlength: 255, class: "form-control js-quick-submit", required: true, autofocus: true
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control js-quick-submit'
- .hint
- .pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
- .pull-left Attach files by dragging & dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
+ = render 'projects/notes/hints'
.clearfix
.error-alert
.col-md-6
@@ -45,7 +35,7 @@
:javascript
- $( ".datepicker" ).datepicker({
+ $(".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/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index 5e93d55b1fb..d6a44c9f0a1 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -18,11 +18,7 @@
.row
.col-sm-6
- - if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
- - if milestone.expires_at
- %span
- = milestone.expires_at
+ = render 'shared/milestone_expired', milestone: milestone
.col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
= link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
@@ -31,4 +27,4 @@
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
= link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
%i.fa.fa-trash-o
- Remove
+ Delete
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index e9dc0b77462..43f8863163d 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,3 +1,9 @@
- page_title "Edit", @milestone.title, "Milestones"
= render "header_title"
+
+%h3.page-title
+ Edit Milestone ##{@milestone.iid}
+
+%hr
+
= render "form"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index a207385bd43..114b06457a5 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,15 +1,18 @@
- page_title "Milestones"
= render "header_title"
-= render 'shared/milestones_filter'
-.gray-content-block
- .pull-right
- - if can? current_user, :admin_milestone, @project
+
+.project-issuable-filter
+ .controls
+ - if can?(current_user, :admin_milestone, @project)
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
%i.fa.fa-plus
New Milestone
- .oneline
- Milestone allows you to group issues and set due date for it
+
+ = render 'shared/milestones_filter'
+
+.gray-content-block
+ Milestone allows you to group issues and set due date for it
.milestones
%ul.content-list
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 9ba9acb6f77..0d016f78313 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,3 +1,9 @@
- page_title "New Milestone"
= render "header_title"
+
+%h3.page-title
+ New Milestone
+
+%hr
+
= render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 3a898dfbcfd..1670ea8741a 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,46 +1,52 @@
-- page_title @milestone.title, "Milestones"
+- page_title @milestone.title, "Milestones"
+- page_description @milestone.description
+
= render "header_title"
-%h4.page-title
- .issue-box{ class: issue_box_class(@milestone) }
+.detail-page-header
+ .status-box{ class: status_box_class(@milestone) }
- if @milestone.closed?
Closed
- elsif @milestone.expired?
Expired
- else
Open
- Milestone ##{@milestone.iid}
- %small.creator
- = @milestone.expires_at
+ %span.identifier
+ Milestone ##{@milestone.iid}
+ - if @milestone.expires_at
+ %span.creator
+ &middot;
+ = @milestone.expires_at
.pull-right
- if can?(current_user, :admin_milestone, @project)
- = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- if @milestone.active?
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
- else
= link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
+
= link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do
%i.fa.fa-trash-o
- Remove
+ Delete
+
+ = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(@milestone.title), pipeline: :single_line
+ %div
+ - if @milestone.description.present?
+ .description
+ .wiki
+ = preserve do
+ = markdown @milestone.description
-%hr
- if @milestone.issues.any? && @milestone.can_be_closed?
- .alert.alert-success
+ .alert.alert-success.prepend-top-default
%span All issues for this milestone are closed. You may close milestone now.
-%h3.issue-title
- = gfm escape_once(@milestone.title)
-%div
- - if @milestone.description.present?
- .description
- .wiki
- = preserve do
- = markdown @milestone.description
-
-%hr
-.context
+.context.prepend-top-default
%p.lead
Progress:
#{@milestone.closed_items_count} closed
@@ -51,8 +57,7 @@
%span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone)
-
-%ul.nav.nav-tabs
+%ul.center-top-menu.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
@@ -66,17 +71,21 @@
Participants
%span.badge= @users.count
- .pull-right
- - if can?(current_user, :create_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- %i.fa.fa-plus
- New Issue
- - if can?(current_user, :read_issue, @project)
- = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn edit-milestone-link btn-grouped"
-
.tab-content
.tab-pane.active#tab-issues
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ - if can?(current_user, :create_issue, @project)
+ = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
+ %i.fa.fa-plus
+ New Issue
+ - if can?(current_user, :read_issue, @project)
+ = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All issues in this milestone
+
+ .row.prepend-top-default
.col-md-4
= render('issues', title: 'Unstarted Issues (open and unassigned)', issues: @issues.opened.unassigned, id: 'unassigned')
.col-md-4
@@ -85,7 +94,15 @@
= render('issues', title: 'Completed Issues (closed)', issues: @issues.closed, id: 'closed')
.tab-pane#tab-merge-requests
- .row
+ .gray-content-block.middle-block
+ .pull-right
+ - if can?(current_user, :read_merge_request, @project)
+ = link_to 'Browse Merge Requests', namespace_project_merge_requests_path(@milestone.project.namespace, @milestone.project, milestone_title: @milestone.title), class: "btn btn-grouped"
+
+ .oneline
+ All merge requests in this milestone
+
+ .row.prepend-top-default
.col-md-3
= render('merge_requests', title: 'Work in progress (open and unassigned)', merge_requests: @merge_requests.opened.unassigned, id: 'unassigned')
.col-md-3
@@ -100,6 +117,10 @@
= render 'merge_request', merge_request: merge_request
.tab-pane#tab-participants
+ .gray-content-block.middle-block
+ .oneline
+ All participants to this milestone
+
%ul.bordered-list
- @users.each do |user|
%li
diff --git a/app/views/projects/network/_head.html.haml b/app/views/projects/network/_head.html.haml
index 415c98ec6a6..28a617538b5 100644
--- a/app/views/projects/network/_head.html.haml
+++ b/app/views/projects/network/_head.html.haml
@@ -1,3 +1,6 @@
-.append-bottom-20
- = render partial: 'shared/ref_switcher', locals: {destination: 'graph'}
- .pull-right.visible-lg.light You can move around the graph by using the arrow keys.
+.gray-content-block.append-bottom-default
+ .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 16005161df6..8065663ca2a 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,5 +1,6 @@
- page_title "Network", @ref
-= header_title project_title(@project, "Network", namespace_project_network_path(@project.namespace, @project, current_ref))
+= render "projects/commits/header_title"
+= render "projects/commits/head"
= render "head"
.project-network
.controls
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index daab2326bc7..25233112132 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,5 +1,10 @@
- page_title 'New Project'
-- header_title 'New Project'
+- header_title "Projects", root_path
+
+%h3.page-title
+ New Project
+%hr
+
.project-edit-container
.project-edit-errors
= render 'projects/errors'
@@ -11,19 +16,23 @@
Project path
.col-sm-10
.input-group
- = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 1, autofocus: true, required: true
- .input-group-addon
- \.git
-
- - if current_user.can_select_namespace?
- .form-group
- = f.label :namespace_id, class: 'control-label' do
- %span Namespace
- .col-sm-10
- = f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2}
+ - 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', 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
@@ -82,19 +91,7 @@
%span Any repo by URL
.js-toggle-content.hide
- .form-group.import-url-data
- = f.label :import_url, class: 'control-label' do
- %span Git repository URL
- .col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
- .well.prepend-top-20
- %ul
- %li
- The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
- %li
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
- %li
- To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
+ = render "shared/import_form", f: f
.prepend-botton-10
@@ -103,19 +100,12 @@
Description
%span.light (optional)
.col-sm-10
- = f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3
+ = 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
.form-actions
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
-
- - if current_user.can_create_group?
- .pull-right
- .light.inline
- .space-right
- Need a group for several dependent projects?
- = link_to new_group_path, class: "btn" do
- Create a group
+ = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel'
.save-project-loader.hide
.center
@@ -124,9 +114,11 @@
Creating project &amp; repository.
%p Please wait a moment, this page will automatically refresh when ready.
-:coffeescript
- $('.how_to_import_link').bind 'click', (e) ->
- e.preventDefault()
- import_modal = $(this).next(".modal").show()
- $('.modal-header .close').bind 'click', ->
- $(".modal").hide()
+:javascript
+ $('.how_to_import_link').bind('click', function (e) {
+ e.preventDefault();
+ var import_modal = $(this).next(".modal").show();
+ });
+ $('.modal-header .close').bind('click', function() {
+ $(".modal").hide();
+ });
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index a21c019986a..3ccda1b381c 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,6 +6,5 @@
= render 'projects/notes/hints'
.note-form-actions
- .buttons
- = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
+ = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
+ = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 13dfa0a1bb3..acb6dc52a8e 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -1,5 +1,5 @@
= form_for [@project.namespace.becomes(Namespace), @project, @note], remote: true, html: { :'data-type' => 'json', multipart: true, id: nil, class: "new_note js-new-note-form common-note-form gfm-form" }, authenticity_token: true do |f|
- = hidden_field_tag :view, params[:view]
+ = hidden_field_tag :view, diff_view
= hidden_field_tag :line_type
= note_target_fields(@note)
= f.hidden_field :commit_id
@@ -12,8 +12,7 @@
= render 'projects/notes/hints'
.error-alert
- .note-form-actions
- .buttons.clearfix
- = f.submit 'Add Comment', class: "btn btn-green comment-btn btn-grouped js-comment-button"
- = yield(:note_actions)
- %a.btn.grouped.js-close-discussion-note-form Cancel
+ .note-form-actions.clearfix
+ = f.submit 'Add Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = yield(:note_actions)
+ %a.btn.btn-cancel.js-close-discussion-note-form Cancel
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 5d184730796..922535e5c4a 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -2,7 +2,7 @@
.timeline-entry-inner
.timeline-icon
%a{href: user_path(note.author)}
- %img.avatar.s40{src: avatar_icon(note.author), alt: ''}
+ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
.timeline-content
.note-header
- if note_editable?(note)
@@ -35,32 +35,11 @@
- if note.updated_by && note.updated_by != note.author
by #{link_to_member(note.project, note.updated_by, avatar: false, author_class: nil)}
- - if note.superceded?(@notes)
- - if note.upvote?
- %span.vote.upvote.label.label-gray.strikethrough
- = icon('thumbs-up')
- \+1
- - if note.downvote?
- %span.vote.downvote.label.label-gray.strikethrough
- = icon('thumbs-down')
- \-1
- - else
- - if note.upvote?
- %span.vote.upvote.label.label-success
- = icon('thumbs-up')
- \+1
- - if note.downvote?
- %span.vote.downvote.label.label-danger
- = icon('thumbs-down')
- \-1
-
-
.note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
- = markdown(note.note, {no_header_anchors: true})
- - unless note.system?
- -# System notes can't be edited
+ = markdown(note.note, pipeline: :note, cache_key: [note, "note"])
+ - if note_editable?(note)
= render 'projects/notes/edit_form', note: note
- if note.attachment.url
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 04222b8f7c4..eb378b42603 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -4,7 +4,7 @@
.js-main-target-form
- if can? current_user, :create_note, @project
- = render "projects/notes/form", view: params[:view]
+ = render "projects/notes/form", view: diff_view
:javascript
- new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{params[:view]}")
+ var notes = new Notes("#{namespace_project_notes_path(namespace_id: @project.namespace, target_id: @noteable.id, target_type: @noteable.class.name.underscore)}", #{@notes.map(&:id).to_json}, #{Time.now.to_i}, "#{diff_view}")
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index 43e92437cf5..1c2458fa144 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -4,12 +4,13 @@
group members
%small
(#{members.count})
- .panel-head-actions
- = link_to group_group_members_path(@group), class: 'btn btn-sm' do
- %i.fa.fa-pencil-square-o
- Edit group members
- %ul.well-list
- - members.each do |member|
+ - if can?(current_user, :admin_group_member, @group)
+ .pull-right
+ = link_to group_group_members_path(@group), class: 'btn' do
+ = icon('pencil-square-o')
+ Manage group members
+ %ul.content-list
+ - members.limit(20).each do |member|
= render 'groups/group_members/group_member', member: member, show_controls: false
- if members.count > 20
%li
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
index 76c46d1d806..05bf3a7ef6a 100644
--- a/app/views/projects/project_members/_project_member.html.haml
+++ b/app/views/projects/project_members/_project_member.html.haml
@@ -4,7 +4,7 @@
%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, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(user, 24), class: "avatar s24", alt: ''
%strong
= link_to user.name, user_path(user)
%span.cgray= user.username
@@ -14,7 +14,7 @@
%label.label.label-danger
%strong Blocked
- else
- = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ = image_tag avatar_icon(member.invite_email, 24), class: "avatar s24", alt: ''
%strong
= member.invite_email
%span.cgray
@@ -24,18 +24,19 @@
= link_to member.created_by.name, user_path(member.created_by)
= time_ago_with_tooltip(member.created_at)
- - if current_user_can_admin_project
+ - 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 current_user_can_admin_project
- - unless @project.personal? && user == current_user
- .pull-right
- %strong= member.human_access
+ - 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
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 615c425e59a..ccddab13aaf 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -1,11 +1,21 @@
-- can_admin_project = can?(current_user, :admin_project, @project)
-
-.panel.panel-default.prepend-top-20
+.panel.panel-default
.panel-heading
%strong #{@project.name}
project members
%small
(#{members.count})
- %ul.well-list
+ .pull-right
+ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control', spellcheck: false }
+ = button_tag class: 'btn', title: 'Search' do
+ = icon("search")
+ %ul.content-list
- members.each do |project_member|
- = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project
+ = render 'project_member', member: project_member
+
+:javascript
+ $('form.member-search-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '?' + $(this).serialize());
+ });
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 82809bec5b8..29225a36364 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,35 +1,21 @@
- page_title "Members"
= render "header_title"
+- @blank_container = true
-.gray-content-block.top-block
- .clearfix.js-toggle-container
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input', spellcheck: false }
- = button_tag 'Search', class: 'btn'
-
- - if can?(current_user, :admin_project_member, @project)
- %span.pull-right
- = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
- Add members
- %i.fa.fa-chevron-down
- = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
- Import members
-
- .js-toggle-content.hide.new-group-member-holder
+.project-members-page
+ - if can?(current_user, :admin_project_member, @project)
+ .panel.panel-default
+ .panel-heading
+ Add new user to project
+ .pull-right
+ = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
+ Import members
+ .panel-body
+ %p.light
+ Users with access to this project are listed below.
= render "new_project_member"
-%p.prepend-top-default.light
- Users with access to this project are listed below.
- Read more about project permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-
-= render "team", members: @project_members
-
-- if @group
- = render "group_members", members: @group_members
+ = render "team", members: @project_members
-:coffeescript
- $('form.member-search-form').on 'submit', (event) ->
- event.preventDefault()
- Turbolinks.visit @.action + '?' + $(@).serialize()
+ - 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 811b1858821..2fb3a41d541 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,2 @@
-- can_admin_project = can?(current_user, :admin_project, @project)
:plain
- $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}');
+ $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member))}');
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index 52b3a50c1e6..cfd7e1534ca 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -3,7 +3,7 @@
%p.light Keep stable branches secure and force developers to use Merge Requests
%hr
-.well.append-bottom-20
+.well
%p Protected branches are designed to
%ul
%li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"}
@@ -22,7 +22,7 @@
.form-group
= f.label :name, "Branch", class: 'control-label'
.col-sm-10
- = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"})
+ = f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: true}, {class: "select2", data: {placeholder: "Select branch"}})
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -33,4 +33,3 @@
.form-actions
= f.submit 'Protect', class: "btn-create btn"
= render 'branches_list'
-
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
new file mode 100644
index 00000000000..bc80f2f29ad
--- /dev/null
+++ b/app/views/projects/releases/edit.html.haml
@@ -0,0 +1,19 @@
+- page_title "Edit", @tag.name, "Tags"
+= render "projects/commits/header_title"
+= render "projects/commits/head"
+
+.gray-content-block
+ .oneline
+ .title
+ Release notes for tag
+ %strong #{@tag.name}
+
+.prepend-top-default
+ = form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal gfm-form release-form' }) do |f|
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', f: f, attr: :description, classes: 'description js-quick-submit form-control'
+ = render 'projects/notes/hints'
+ .error-alert
+ .form-actions.prepend-top-default
+ = f.submit 'Save changes', class: 'btn btn-save'
+ = link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index 07c24950ee2..b9486a9b492 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -3,10 +3,10 @@
- split_button = split_button || false
- if split_button == true
%span.btn-group{class: btn_class}
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn btn-success col-xs-10', rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do
%i.fa.fa-download
%span Download zip
- %a.col-xs-2.btn.btn-success.dropdown-toggle{ 'data-toggle' => 'dropdown' }
+ %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
%span.caret
%span.sr-only
Select Archive Format
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index f3526ad0747..6ca919f7f80 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: ''
- = gfm escape_once(truncate(commit.title, length: 40))
+ = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line
%td
%span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index e6b8a2e6fe7..47ec420189d 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -15,10 +15,10 @@
- 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
- - runner_project = @ci_project.runner_projects.find_by(runner_id: runner)
- = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - runner_project = @project.runner_projects.find_by(runner_id: runner)
+ = link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- elsif runner.specific?
- = form_for [:ci, @ci_project, @ci_project.runner_projects.new] do |f|
+ = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f|
= f.hidden_field :runner_id, value: runner.id
= f.submit 'Enable for this project', class: 'btn btn-sm'
.pull-right
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 316ea747b14..6a37f444bb7 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -3,17 +3,17 @@
.bs-callout.bs-callout-warning
GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
%hr
- - if @ci_project.shared_runners_enabled
- = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do
+ - if @project.shared_runners_enabled?
+ = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do
Disable shared runners
- else
- = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do
+ = link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do
Enable shared runners
&nbsp; for this project
- if @shared_runners_count.zero?
- This application has no shared runners yet.
- Please use specific runners or ask administrator to create one
+ This GitLab server does not provide any shared runners yet.
+ Please use specific runners or ask the administrator to create one.
- else
%h4.underlined-title Available shared runners - #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index c13625c7e49..30cd1263a12 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -12,7 +12,7 @@
%code #{ci_root_url(only_path: false)}
%li
Use the following registration token during setup:
- %code #{@ci_project.token}
+ %code #{@project.runners_token}
%li
Start runner!
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
index 66851d38316..eba03028af8 100644
--- a/app/views/projects/runners/edit.html.haml
+++ b/app/views/projects/runners/edit.html.haml
@@ -1,3 +1,5 @@
+- 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|
@@ -21,7 +23,7 @@
= label_tag :tag_list, class: 'control-label' do
Tags
.col-sm-10
- = f.text_field :tag_list, class: 'form-control'
+ = 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', class: 'btn btn-save'
+ = f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 529fb9c296d..315afe4a764 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Runners"
.light
%p
A 'runner' is a process which runs a build.
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index c255cd51bd2..5bf4c09ca25 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -1,13 +1,14 @@
-= content_for :title do
- %h3.project-title
- Runner ##{@runner.id}
- .pull-right
- - if @runner.shared?
- %span.runner-state.runner-state-shared
- Shared
- - else
- %span.runner-state.runner-state-specific
- Specific
+- page_title "#{@runner.description} ##{@runner.id}", "Runners"
+
+%h3.page-title
+ Runner ##{@runner.id}
+ .pull-right
+ - if @runner.shared?
+ %span.runner-state.runner-state-shared
+ Shared
+ - else
+ %span.runner-state.runner-state-specific
+ Specific
.table-holder
%table.table
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index e1823b51198..1b70880043a 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -4,18 +4,15 @@
%p= @service.description
-.back-link
- = link_to namespace_project_services_path(@project.namespace, @project) do
- &larr; to services
-
%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', class: 'btn btn-save'
+ = 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/show.atom.builder b/app/views/projects/show.atom.builder
index 242684e5c7c..15c49767556 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project)
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 585caf674c9..ffbe445b447 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -11,7 +11,7 @@
= render "home_panel"
-.project-stats.gray-content-block
+.project-stats.gray-content-block.second-block
%ul.nav.nav-pills
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
@@ -68,15 +68,4 @@
= render 'projects/last_commit', commit: @repository.commit, project: @project
%div{class: "project-show-#{default_project_view}"}
- = render default_project_view
-
-- if current_user
- - access = user_max_access_in_project(current_user, @project)
- - if access
- .prepend-top-20.project-footer
- .gray-content-block.footer-block.center
- You have #{access} access to this project.
- - if @project.project_member_by_id(current_user)
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
- Leave this project
+ = render default_project_view \ No newline at end of file
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
new file mode 100644
index 00000000000..4a515469422
--- /dev/null
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -0,0 +1,11 @@
+= 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
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index e69f2d99709..dc3ea1fcf12 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -2,6 +2,6 @@
= render "header_title"
%h3.page-title
- Edit snippet
+ Edit Snippet
%hr
= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 3fed2c9949d..4af963e14da 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,17 +1,13 @@
- page_title "Snippets"
= render "header_title"
-%h3.page-title
- Snippets
- - if can? current_user, :create_project_snippet, @project
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new pull-right", title: "New Snippet" do
- Add new snippet
+.gray-content-block.top-block
+ .pull-right
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
+ = icon('plus')
+ New Snippet
-%p.light
- Share code pastes with others out of git repository
+ .oneline
+ Share code pastes with others out of git repository
-%ul.bordered-list
- = render partial: "shared/snippets/snippet", collection: @snippets
- - if @snippets.empty?
- %li
- .nothing-here-block Nothing here.
+= render 'snippets/snippets'
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index 67cd69fd215..e57237991b4 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -2,6 +2,6 @@
= render "header_title"
%h3.page-title
- New snippet
+ New Snippet
%hr
= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index be7d4d486fa..7c599563ce4 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,40 +1,18 @@
- page_title @snippet.title, "Snippets"
= render "header_title"
-%h3.page-title
- = @snippet.title
+.snippet-holder
+ = render 'shared/snippets/header'
- .pull-right
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
- Add new snippet
+ %article.file-holder
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ %strong
+ = @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"
-%hr
+ = render 'shared/snippets/blob'
-.append-bottom-20
- .pull-right
- = "##{@snippet.id}"
- %span.light
- by
- = link_to user_path(@snippet.author) do
- = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16"
- = @snippet.author_name
-
- .back-link
- = link_to namespace_project_snippets_path(@project.namespace, @project) do
- &larr; project snippets
-
-.file-holder
- .file-title
- %i.fa.fa-file
- %strong
- = @snippet.file_name
- .file-actions
- .btn-group
- - if can?(current_user, :update_project_snippet, @snippet)
- = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet'
- = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- - if can?(current_user, :admin_project_snippet, @snippet)
- = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
- = render 'shared/snippets/blob'
-
-%div#notes= render "projects/notes/notes_with_form"
+ %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
new file mode 100644
index 00000000000..667057ef2d8
--- /dev/null
+++ b/app/views/projects/tags/_download.html.haml
@@ -0,0 +1,17 @@
+%span.btn-group.btn-grouped
+ = 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
+ %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
+ %span.caret
+ %span.sr-only
+ Select Archive Format
+ %ul.col-xs-10.dropdown-menu{ 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 2ca295fc5f3..28b706c5c7e 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,22 +1,34 @@
- commit = @repository.commit(tag.target)
+- release = @releases.find { |release| release.tag == tag.name }
%li
%div
- = link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
%strong
- %i.fa.fa-tag
+ = icon('tag')
= tag.name
- if tag.message.present?
&nbsp;
= strip_gpg_signature(tag.message)
+
.controls
- - if can? current_user, :download_code, @project
- = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs'
+ - if can?(current_user, :download_code, @project)
+ = 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
+ = icon("pencil")
+
- if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do
- %i.fa.fa-trash-o
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{tag.name}' tag cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = icon("trash-o")
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
%p
Cant find HEAD commit for this tag
+ - if release && release.description.present?
+ .description.prepend-top-default
+ .wiki
+ = preserve do
+ = markdown release.description
diff --git a/app/views/projects/tags/destroy.js.haml b/app/views/projects/tags/destroy.js.haml
deleted file mode 100644
index ada6710f940..00000000000
--- a/app/views/projects/tags/destroy.js.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-$('.js-totaltags-count').html("#{@repository.tags.size}")
-- if @repository.tags.size == 0
- $('.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 85d76eae3b5..760347de0a9 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -6,7 +6,7 @@
- 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
- %i.fa.fa-add-sign
+ = icon('plus')
New tag
.oneline
Tags give the ability to mark specific points in history as being important
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 9f5c1be125c..3a2f75fecaa 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -5,33 +5,42 @@
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
= @error
+
%h3.page-title
- %i.fa.fa-code-fork
- New tag
-= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal" do
+ New Tag
+%hr
+
+= form_tag namespace_project_tags_path, method: :post, id: "new-tag-form", class: "form-horizontal gfm-form tag-form js-requires-input" do
.form-group
- = label_tag :tag_name, 'Name for new tag', class: 'control-label'
+ = label_tag :tag_name, nil, class: 'control-label'
.col-sm-10
- = text_field_tag :tag_name, params[:tag_name], placeholder: 'v3.0.1', required: true, tabindex: 1, class: 'form-control'
+ = text_field_tag :tag_name, params[:tag_name], required: true, tabindex: 1, autofocus: true, class: 'form-control'
.form-group
= label_tag :ref, 'Create from', class: 'control-label'
.col-sm-10
- = text_field_tag :ref, params[:ref], placeholder: 'master', required: true, tabindex: 2, class: 'form-control'
- .light Branch name or commit SHA
+ = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+ .help-block Branch name or commit SHA
+ .form-group
+ = label_tag :message, nil, class: 'control-label'
+ .col-sm-10
+ = text_field_tag :message, nil, required: false, tabindex: 3, class: 'form-control'
+ .help-block Optionally, enter a message to create an annotated tag.
+ %hr
.form-group
- = label_tag :message, 'Message', class: 'control-label'
+ = label_tag :release_description, 'Release notes', class: 'control-label'
.col-sm-10
- = text_field_tag :message, nil, placeholder: 'Enter message.', required: false, tabindex: 3, class: 'form-control'
- .light (Optional) Entering a message will create an annotated tag.
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
+ = render 'projects/zen', attr: :release_description, classes: 'description js-quick-submit form-control'
+ = render 'projects/notes/hints'
+ .help-block Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.
.form-actions
= button_tag 'Create tag', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', namespace_project_tags_path(@project.namespace, @project), class: 'btn btn-cancel'
:javascript
- disableButtonIfAnyEmptyField($("#new-tag-form"), ".form-control", ".btn-create");
- var availableTags = #{@project.repository.ref_names.to_json};
+ var availableRefs = #{@project.repository.ref_names.to_json};
$("#ref").autocomplete({
- source: availableTags,
+ source: availableRefs,
minLength: 1
});
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
new file mode 100644
index 00000000000..b594d4f1f27
--- /dev/null
+++ b/app/views/projects/tags/show.html.haml
@@ -0,0 +1,39 @@
+- page_title @tag.name, "Tags"
+= render "projects/commits/header_title"
+= render "projects/commits/head"
+
+.gray-content-block
+ .pull-right
+ - if can?(current_user, :push_code, @project)
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, @tag.name), class: 'btn-grouped btn has_tooltip', title: 'Edit release notes' do
+ = icon("pencil")
+ = link_to namespace_project_tree_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse files' do
+ = icon('files-o')
+ = link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn btn-grouped has_tooltip', title: 'Browse commits' do
+ = icon('history')
+ - if can? current_user, :download_code, @project
+ = render 'projects/tags/download', ref: @tag.name, project: @project
+ - if can?(current_user, :admin_project, @project)
+ .pull-right
+ = link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
+ %i.fa.fa-trash-o
+ .title
+ %strong= @tag.name
+ - if @tag.message.present?
+ %span.light
+ &nbsp;
+ = strip_gpg_signature(@tag.message)
+ - if @commit
+ = render 'projects/branches/commit', commit: @commit, project: @project
+ - else
+ Cant find HEAD commit for this tag
+
+
+.append-bottom-default.prepend-top-default
+ - if @release.description.present?
+ .description
+ .wiki
+ = preserve do
+ = markdown @release.description
+ - else
+ This tag has no release notes.
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index ee4c9d1693d..1927883513a 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -12,7 +12,7 @@
%i.fa.fa-angle-right
&nbsp;
%small.light
- = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit)
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
= truncate(@commit.title, length: 50)
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
@@ -29,8 +29,8 @@
- if tree.readme
= render "projects/tree/readme", readme: tree.readme
-- if allowed_tree_edit?
- = render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
+- if can_edit_tree?
+ = render 'projects/blob/upload', title: 'Upload New File', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
= render 'projects/blob/new_dir'
:javascript
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 1115ca6b4ca..3343288ad2b 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -11,22 +11,65 @@
= link_to truncate(title, length: 40), namespace_project_tree_path(@project.namespace, @project, path)
- else
= link_to title, '#'
- - if allowed_tree_edit?
+
+ - if current_user
%li
- %span.dropdown
- %a.dropdown-toggle.btn.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ - if !on_top_of_branch?
+ %span.btn.btn-sm.add-to-tree.disabled.has_tooltip{title: "You can only add files when you are on a branch", data: { container: 'body' }}
= icon('plus')
- %ul.dropdown-menu
- %li
- = link_to namespace_project_new_blob_path(@project.namespace, @project, @id), title: 'Create file', id: 'new-file-link' do
- = icon('pencil fw')
- Create file
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
- = icon('file fw')
- Upload file
- %li.divider
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
- = icon('folder fw')
- New directory
+ - else
+ %span.dropdown
+ %a.dropdown-toggle.btn.btn-sm.add-to-tree{href: '#', "data-toggle" => "dropdown"}
+ = icon('plus')
+ %ul.dropdown-menu
+ - if can_edit_tree?
+ %li
+ = link_to namespace_project_new_blob_path(@project.namespace, @project, @id) do
+ = icon('pencil fw')
+ New file
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'} do
+ = icon('file fw')
+ Upload file
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal'} do
+ = icon('folder fw')
+ New directory
+ - elsif can?(current_user, :fork_project, @project)
+ %li
+ - continue_params = { to: namespace_project_new_blob_path(@project.namespace, @project, @id),
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('pencil fw')
+ New file
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to upload a file again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('file fw')
+ Upload file
+ %li
+ - continue_params = { to: request.fullpath,
+ notice: edit_in_new_fork_notice + " Try to create a new directory again.",
+ notice_now: edit_in_new_fork_notice_now }
+ - fork_path = namespace_project_fork_path(@project.namespace, @project, namespace_key: current_user.namespace.id,
+ continue: continue_params)
+ = link_to fork_path, method: :post do
+ = icon('folder fw')
+ New directory
+
+ %li.divider
+ %li
+ = link_to new_namespace_project_branch_path(@project.namespace, @project) do
+ = icon('code-fork fw')
+ New branch
+ %li
+ = link_to new_namespace_project_tag_path(@project.namespace, @project) do
+ = icon('tags fw')
+ New tag
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index 18a37302c3e..bd346c4b8e6 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Triggers"
%h3.page-title
Triggers
@@ -35,7 +36,8 @@
:plain
curl -X POST \
-F token=TOKEN \
- #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}
+ -F ref=REF_NAME \
+ #{builds_trigger_url(@project.id)}
%h3
Use .gitlab-ci.yml
@@ -50,7 +52,7 @@
trigger:
type: deploy
script:
- - "curl -X POST -F token=TOKEN #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}"
+ - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
%h3
Pass build variables
@@ -64,5 +66,6 @@
:plain
curl -X POST \
-F token=TOKEN \
+ -F "ref=REF_NAME" \
-F "variables[RUN_NIGHTLY_BUILD]=true" \
- #{ci_build_trigger_url(@ci_project.id, 'REF_NAME')}
+ #{builds_trigger_url(@project.id)}
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
index 29416a94ff6..e80dffc1ced 100644
--- a/app/views/projects/variables/show.html.haml
+++ b/app/views/projects/variables/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Variables"
%h3.page-title
Secret Variables
@@ -9,13 +10,13 @@
%hr
-= nested_form_for @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
+= nested_form_for @project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
- if @project.errors.any?
#error_explanation
- %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
+ %p.lead= "#{pluralize(@project.errors.count, "error")} prohibited this project from being saved:"
.alert.alert-error
%ul
- - @ci_project.errors.full_messages.each do |msg|
+ - @project.errors.full_messages.each do |msg|
%li= msg
= f.fields_for :variables do |variable_form|
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 261d4a92d7d..1d257818dcd 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form' } do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form gfm-form prepend-top-default' } do |f|
-if @page.errors.any?
#error_explanation
.alert.alert-danger
@@ -11,24 +11,20 @@
.col-sm-10
= f.select :format, options_for_select(ProjectWiki::MARKUPS, {selected: @page.format}), {}, class: "form-control"
- .row
- .col-sm-offset-2.col-sm-10
- %p.cgray
- To link to a (new) page you can just type
- %code [Link Title](page-slug)
- \.
-
- .form-group.wiki-content
+ .form-group
= f.label :content, class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control js-quick-submit'
- .col-sm-12.hint
- .pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}
- .pull-right Attach files by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector' }.
+ = render 'projects/notes/hints'
.clearfix
.error-alert
+
+ .help-block
+ To link to a (new) page, simply type
+ %code [Link Title](page-slug)
+ \.
.form-group
= f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 14f25822259..29bf5d62abe 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,9 +1,4 @@
%span.pull-right
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new btn-grouped", "data-toggle" => "modal" do
- %i.fa.fa-plus
- New Page
-
- if (@page && @page.persisted?)
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
Page History
@@ -11,5 +6,7 @@
= link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
-
-= render 'projects/wikis/new'
+ - 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 fffb4eb31ab..e6e6ad5bc4b 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,10 +1,19 @@
-%ul.center-top-menu
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+.project-issuable-filter
+ .controls
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ %i.fa.fa-plus
+ New Page
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+ = render 'projects/wikis/new'
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
+ %ul.center-top-menu
+ = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
+ = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+
+ = nav_link(path: 'wikis#pages') do
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+
+ = nav_link(path: 'wikis#git_access') do
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+ Git Access
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index dace172438c..f0547e9c057 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -12,5 +12,5 @@
The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
%p.hint
Please don't use spaces.
- .modal-footer
- = link_to 'Build', '#', class: 'build-new-wiki btn btn-create'
+ .form-actions
+ = link_to '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 0b709c3695b..23f64fbbd10 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,16 +1,16 @@
-- page_title "Edit", @page.title, "Wiki"
+- page_title "Edit", @page.title.capitalize, "Wiki"
= render "header_title"
= render 'nav'
-.pull-right
- = render 'main_links'
-%h3.page-title
- Editing -
- %span.light #{@page.title}
-%hr
-= render 'form'
+.gray-content-block
+ .pull-right
+ = render 'main_links'
+
+ %h3.page-title.oneline
+ %span.light Edit Page
+ - if @page.persisted?
+ = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title
-.pull-right
- - if @page.persisted? && 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-sm btn-remove" do
- Delete this page
+= render 'form'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 6417ef4a38b..11c8c4f0eba 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -5,7 +5,7 @@
.gray-content-block
.row
.col-sm-6
- %h3.page-title
+ %h3.page-title.oneline
Git access for
%strong= @project_wiki.path_with_namespace
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index d179a1abec1..aae1ad69ad9 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,11 +1,10 @@
-- page_title "All Pages", "Wiki"
+- page_title "Pages", "Wiki"
= render "header_title"
= render 'nav'
.gray-content-block
- = render 'main_links'
- %h3.page-title
- All Pages
+ All pages in this wiki are listed below.
+
%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 55fbf5a8b6e..309d40f52bc 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -5,11 +5,12 @@
.gray-content-block
= render 'main_links'
- %h3.page-title
+ %h3.page-title.oneline
= @page.title.capitalize
- .wiki-last-edit-by
- 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)}
- if @page.historical?
.warning_message
@@ -21,8 +22,3 @@
.wiki
= preserve do
= render_wiki_content(@page)
-
-.gray-content-block.footer-block
- .wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
-
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index d637abfa76b..481451edb23 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -42,6 +42,13 @@
Wiki
%span.badge
= @search_results.wiki_blobs_count
+ %li{class: ("active" if @scope == 'commits')}
+ = link_to search_filter_path(scope: 'commits') do
+ = icon('history fw')
+ %span
+ Commits
+ %span.badge
+ = @search_results.commits_count
- elsif @show_snippets
%li{class: ("active" if @scope == 'snippet_blobs')}
diff --git a/app/views/search/results/_commit.html.haml b/app/views/search/results/_commit.html.haml
new file mode 100644
index 00000000000..4e6c3965dc6
--- /dev/null
+++ b/app/views/search/results/_commit.html.haml
@@ -0,0 +1,2 @@
+.search-result-row
+ = render 'projects/commits/commit', project: @project, commit: commit
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index ce8ddff9556..45d700781f3 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -6,7 +6,7 @@
- if issue.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(issue.description))
+ = search_md_sanitize(markdown(issue.description, { project: issue.project }))
%span.light
#{issue.project.name_with_namespace}
- if issue.closed?
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 2e4aab36301..687a59c270f 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,27 +1,27 @@
- project = project || @project
-.git-clone-holder.input-group
- .input-group-addon.git-protocols
- .input-group-btn
- %button{ |
- type: 'button', |
- class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
- :"data-clone" => project.ssh_url_to_repo, |
- :"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH.",
- :"data-html" => "true",
- :"data-container" => "body"}
- SSH
- .input-group-btn
- %button{ |
- type: 'button', |
- class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
- :"data-clone" => project.http_url_to_repo, |
- :"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}.",
- :"data-html" => "true",
- :"data-container" => "body"}
- = gitlab_config.protocol.upcase
+
+.git-clone-holder
+ .btn-group.clone-options
+ %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
+ %a#ssh-selector{href: @project.ssh_url_to_repo}
+ SSH
+ %li
+ %a#http-selector{href: @project.http_url_to_repo}
+ HTTPS
+
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
- - if project.kind_of?(Project)
- .input-group-addon
- .visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" }
- = visibility_level_icon(project.visibility_level)
-
+ .input-group-btn
+ = clipboard_button(clipboard_target: '#project_clone')
+
+:javascript
+ $('ul.clone-options-dropdown a').on('click',function(e){
+ e.preventDefault();
+ var $this = $(this);
+ $('a.clone-dropdown-btn span').text($this.text());
+ $('#project_clone').val($this.attr('href'));
+ });
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index cc3f1268f8b..7c57924277e 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,13 +1,15 @@
.form-group.commit_message-group
- = label_tag 'commit_message', class: 'control-label' do
+ - nonce = SecureRandom.hex
+ = label_tag "commit_message-#{nonce}", class: 'control-label' do
Commit message
.col-sm-10
.commit-message-container
.max-width-marker
= text_area_tag 'commit_message',
(params[:commit_message] || local_assigns[:text]),
- class: 'form-control js-quick-submit', placeholder: local_assigns[:placeholder],
- required: true, rows: (local_assigns[:rows] || 3)
+ class: 'form-control js-commit-message js-quick-submit', placeholder: local_assigns[:placeholder],
+ required: true, rows: (local_assigns[:rows] || 3),
+ id: "commit_message-#{nonce}"
- if local_assigns[:hint]
%p.hint
Try to keep the first line under 52 characters
diff --git a/app/views/shared/_confirm_modal.html.haml b/app/views/shared/_confirm_modal.html.haml
index 5f51b0d450f..34241cd8aad 100644
--- a/app/views/shared/_confirm_modal.html.haml
+++ b/app/views/shared/_confirm_modal.html.haml
@@ -3,7 +3,8 @@
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} ×
- %h4 Confirmation required
+ %h3.page-title
+ Confirmation required
.modal-body
%p.cred.lead.js-confirm-text
@@ -14,9 +15,9 @@
%br
Please type
%code.js-confirm-danger-match #{phrase}
- to proceed or close this modal to cancel
+ to proceed or close this modal to cancel.
.form-group
= text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input'
- .form-group
+ .form-actions
= submit_tag 'Confirm', class: "btn btn-danger js-confirm-danger-submit"
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 57c3aff3e18..2bc98983d67 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -8,5 +8,6 @@
%a{href: "#L#{i}", id: "L#{i}", 'data-line-number' => i}
%i.fa.fa-link
= i
- :preserve
- #{highlight(blob.name, blob.data)}
+ .blob-content{data: {blob_id: blob.id}}
+ :preserve
+ #{highlight(blob.name, blob.data)}
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index c0a9923348e..67072b9fc2a 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -23,7 +23,7 @@
%li It will change the git path to repositories under this group.
.form-group.group-description-holder
- = f.label :description, 'Details', class: 'control-label'
+ = f.label :description, class: 'control-label'
.col-sm-10
= f.text_area :description, maxlength: 250,
class: 'form-control js-gfm-input', rows: 4
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
new file mode 100644
index 00000000000..285af56ad73
--- /dev/null
+++ b/app/views/shared/_import_form.html.haml
@@ -0,0 +1,16 @@
+.form-group.import-url-data
+ = f.label :import_url, class: 'control-label' do
+ %span Git repository URL
+ .col-sm-10
+ = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+
+ .well.prepend-top-20
+ %ul
+ %li
+ The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
+ %li
+ If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ %li
+ The import will time out after 4 minutes. For big repositories, use a clone/push combination.
+ %li
+ To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/importing/migrating_from_svn.html"}.
diff --git a/app/views/shared/_issues.html.haml b/app/views/shared/_issues.html.haml
index 0dbb6a04393..4b4c9e9eabe 100644
--- a/app/views/shared/_issues.html.haml
+++ b/app/views/shared/_issues.html.haml
@@ -3,8 +3,10 @@
.panel.panel-default.panel-small
- project = group[0]
.panel-heading
- = link_to_project project
- = link_to 'show all', namespace_project_issues_path(project.namespace, project), class: 'pull-right'
+ = link_to project.name_with_namespace, namespace_project_issues_path(project.namespace, project)
+ - if can?(current_user, :create_issue, project)
+ .pull-right
+ = link_to 'New issue', new_namespace_project_issue_path(project.namespace, project)
%ul.well-list.issues-list
- group[1].each do |issue|
@@ -12,4 +14,3 @@
= paginate @issues, theme: "gitlab"
- else
.nothing-here-block No issues to show
-
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index da49c48acd3..3d279ec228c 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -5,13 +5,13 @@
<g id="Fill-1-+-Group-24">
<g id="Group-24">
<g id="Group">
- <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path>
- <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path>
- <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path>
- <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path>
+ <path id="tanuki-right-ear" d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" fill="#E24329" class="tanuki-shape"></path>
+ <path id="tanuki-right-cheek" d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path>
+ <path id="tanuki-right-eye" d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path>
+ <path id="tanuki-nose" d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" fill="#E24329" class="tanuki-shape"></path>
+ <path id="tanuki-left-eye" d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" fill="#FC6D26" class="tanuki-shape"></path>
+ <path id="tanuki-left-cheek" d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" fill="#FCA326" class="tanuki-shape"></path>
+ <path id="tanuki-left-ear" d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" fill="#E24329" class="tanuki-shape"></path>
</g>
</g>
</g>
diff --git a/app/views/shared/_merge_requests.html.haml b/app/views/shared/_merge_requests.html.haml
index c02c5af008a..be17a511b26 100644
--- a/app/views/shared/_merge_requests.html.haml
+++ b/app/views/shared/_merge_requests.html.haml
@@ -3,8 +3,11 @@
.panel.panel-default.panel-small
- project = group[0]
.panel-heading
- = link_to_project project
- = link_to 'show all', namespace_project_merge_requests_path(project.namespace, project), class: 'pull-right'
+ = link_to project.name_with_namespace, namespace_project_merge_requests_path(project.namespace, project)
+ - if can?(current_user, :create_merge_request, project)
+ .pull-right
+ = link_to 'New merge request', new_namespace_project_merge_request_path(project.namespace, project)
+
%ul.well-list.mr-list
- group[1].each do |merge_request|
= render 'projects/merge_requests/merge_request', merge_request: merge_request
diff --git a/app/views/shared/_milestone_expired.html.haml b/app/views/shared/_milestone_expired.html.haml
new file mode 100644
index 00000000000..b8eef15fbec
--- /dev/null
+++ b/app/views/shared/_milestone_expired.html.haml
@@ -0,0 +1,5 @@
+- if milestone.expired? and not milestone.closed?
+ %span.cred (Expired)
+- if milestone.expires_at
+ %span
+ = milestone.expires_at
diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml
new file mode 100644
index 00000000000..0c8ac48bb58
--- /dev/null
+++ b/app/views/shared/_new_commit_form.html.haml
@@ -0,0 +1,22 @@
+= render 'shared/commit_message_container', placeholder: placeholder
+
+- if @project.empty_repo?
+ = hidden_field_tag 'target_branch', @ref
+- else
+ - if can?(current_user, :push_code, @project)
+ .form-group.branch
+ = label_tag 'target_branch', 'Target branch', class: 'control-label'
+ .col-sm-10
+ = text_field_tag 'target_branch', @target_branch || tree_edit_branch, required: true, class: "form-control js-target-branch"
+
+ .js-create-merge-request-container
+ .checkbox
+ - nonce = SecureRandom.hex
+ = label_tag "create_merge_request-#{nonce}" do
+ = check_box_tag 'create_merge_request', 1, true, class: 'js-create-merge-request', id: "create_merge_request-#{nonce}"
+ Start a <strong>new merge request</strong> with these changes
+ - else
+ = hidden_field_tag 'target_branch', @target_branch || tree_edit_branch
+ = hidden_field_tag 'create_merge_request', 1
+
+ = hidden_field_tag 'original_branch', @ref, class: 'js-original-branch'
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
new file mode 100644
index 00000000000..c4431d66927
--- /dev/null
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -0,0 +1,20 @@
+- if @projects.any?
+ .prepend-left-10.new-project-item-select-holder
+ = project_select_tag :project_path, class: "new-project-item-select", data: { include_groups: local_assigns[:include_groups] }
+ %a.btn.btn-new.new-project-item-select-button
+ = icon('plus')
+ = local_assigns[:label]
+ %b.caret
+
+ :javascript
+ $('.new-project-item-select-button').on('click', function() {
+ $('.new-project-item-select').select2('open');
+ });
+
+ var relativePath = '#{local_assigns[:path]}';
+
+ $('.new-project-item-select').on('click', function() {
+ window.location = $(this).val() + '/' + relativePath;
+ });
+
+ new ProjectSelect()
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
new file mode 100644
index 00000000000..960ff00b49d
--- /dev/null
+++ b/app/views/shared/_project_limit.html.haml
@@ -0,0 +1,8 @@
+- if cookies[:hide_project_limit_message].blank? && !current_user.hide_project_limit && !current_user.can_create_project?
+ .project-limit-message.alert.alert-warning.hidden-xs
+ You won't be able to create new projects because you have reached your project limit.
+
+ .pull-right
+ = link_to "Don't show again", profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link'
+ |
+ = link_to 'Remind later', '#', class: 'hide-project-limit-message alert-link'
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 16a98a7233c..28d6f421fea 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -59,6 +59,15 @@
%strong Merge Request events
%p.light
This url will be triggered when a merge request is created
+ - if @service.supported_events.include?("build")
+ %div
+ = form.check_box :build_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :build_events, class: 'list-label' do
+ %strong Build events
+ %p.light
+ This url will be triggered when a build status changes
+
- @service.fields.each do |field|
- type = field[:type]
diff --git a/app/views/shared/issuable/_context.html.haml b/app/views/shared/issuable/_context.html.haml
deleted file mode 100644
index cba18c14568..00000000000
--- a/app/views/shared/issuable/_context.html.haml
+++ /dev/null
@@ -1,50 +0,0 @@
-= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
- %div.prepend-top-20
- .issuable-context-title
- %label
- Assignee:
- - if issuable.assignee
- %strong= link_to_member(@project, issuable.assignee, size: 24)
- - else
- none
- .issuable-context-selectbox
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true)
-
- %div.prepend-top-20.clearfix
- .issuable-context-title
- %label
- Milestone:
- - if issuable.milestone
- %span.back-to-milestone
- = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
- %strong
- = icon('clock-o')
- = issuable.milestone.title
- - else
- none
- .issuable-context-selectbox
- - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
- = f.select(:milestone_id, milestone_options(issuable), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'})
- = hidden_field_tag :issuable_context
- = f.submit class: 'btn hide'
-
- - if current_user
- - subscribed = issuable.subscribed?(current_user)
- %div.prepend-top-20.clearfix
- .issuable-context-title
- %label
- Subscription:
- %button.btn.btn-block.subscribe-button{:type => 'button'}
- = icon('eye')
- %span= subscribed ? 'Unsubscribe' : 'Subscribe'
- - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
- .subscription-status{data: {status: subscribtion_status}}
- .description-block.unsubscribed{class: ( 'hidden' if subscribed )}
- You're not receiving notifications from this thread.
- .description-block.subscribed{class: ( 'hidden' unless subscribed )}
- You're receiving notifications because you're subscribed to this thread.
-
-:coffeescript
- new Subscription("#{toggle_subscription_path(issuable)}")
- new IssuableContext()
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 0e4e9c0987a..be06738eac9 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -30,12 +30,12 @@
class: "check_all_issues left"
.issues-other-filters
.filter-item.inline
- = users_select_tag(:assignee_id, selected: params[:assignee_id],
- placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true, current_user: true)
+ = users_select_tag(:author_id, selected: params[:author_id],
+ placeholder: 'Author', class: 'trigger-submit', any_user: "Any Author", first_user: true, current_user: true)
.filter-item.inline
- = users_select_tag(:author_id, selected: params[:author_id],
- placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true, current_user: true)
+ = users_select_tag(:assignee_id, selected: params[:assignee_id],
+ placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
.filter-item.inline.milestone-filter
= select_tag('milestone_title', projects_milestones_options,
@@ -53,16 +53,20 @@
- if controller.controller_name == 'issues'
.issues_bulk_update.hide
= form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control')
- = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
- = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone")
+ .filter-item.inline
+ = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), include_blank: true, data: { placeholder: "Status" })
+ .filter-item.inline
+ = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true, first_user: true, current_user: true)
+ .filter-item.inline
+ = select_tag('update[milestone_id]', bulk_update_milestone_options, include_blank: true, data: { placeholder: "Milestone" })
= hidden_field_tag 'update[issues_ids]', []
= hidden_field_tag :state_event, params[:state_event]
- = button_tag "Update issues", class: "btn update_selected_issues btn-save"
-
-:coffeescript
- new UsersSelect()
+ .filter-item.inline
+ = button_tag "Update issues", class: "btn update_selected_issues btn-save"
- $('form.filter-form').on 'submit', (event) ->
- event.preventDefault()
- Turbolinks.visit @.action + '&' + $(@).serialize()
+:javascript
+ new UsersSelect();
+ $('form.filter-form').on('submit', function (event) {
+ event.preventDefault();
+ Turbolinks.visit(this.action + '&' + $(this).serialize());
+ });
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 594e54f404c..90dc0062481 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -6,8 +6,7 @@
%span= msg
%br
.form-group
- = f.label :title, class: 'control-label' do
- %strong= 'Title *'
+ = f.label :title, class: 'control-label'
.col-sm-10
= f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad js-gfm-input js-quick-submit', required: true
@@ -20,46 +19,35 @@
- else
Start the title with <code>[WIP]</code> or <code>WIP:</code> to prevent a
<strong>Work In Progress</strong> merge request from being merged before it's ready.
-.form-group.issuable-description
+.form-group.detail-page-description
= f.label :description, 'Description', class: 'control-label'
.col-sm-10
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
classes: 'description form-control js-quick-submit'
- .col-sm-12.hint
- .pull-left
- Parsed with
- #{link_to 'GitLab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}.
- .pull-right
- Attach files by dragging &amp; dropping
- or #{link_to 'selecting them', '#', class: 'markdown-selector' }.
-
+ = render 'projects/notes/hints'
.clearfix
.error-alert
- %hr
- if can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project)
+ %hr
.form-group
.issue-assignee
- = f.label :assignee_id, class: 'control-label' do
- %i.fa.fa-user
- Assign to
+ = f.label :assignee_id, "Assignee", class: 'control-label'
.col-sm-10
= users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select a user', class: 'custom-form-control', null_user: true,
+ placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
selected: issuable.assignee_id, project: @target_project || @project,
- first_user: true, current_user: true)
+ 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, class: 'control-label' do
- %i.fa.fa-clock-o
- Milestone
+ = f.label :milestone_id, "Milestone", class: 'control-label'
.col-sm-10
- if milestone_options(issuable).present?
= f.select(:milestone_id, milestone_options(issuable),
- { include_blank: 'Select milestone' }, { class: 'select2' })
+ { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- else
.prepend-top-10
%span.light No open milestones available.
@@ -67,13 +55,11 @@
- 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
- = f.label :label_ids, class: 'control-label' do
- %i.fa.fa-tag
- Labels
+ = f.label :label_ids, "Labels", class: 'control-label'
.col-sm-10
- if issuable.project.labels.any?
= f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2'
+ { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else
.prepend-top-10
%span.light No labels yet.
@@ -85,31 +71,30 @@
%hr
- if @merge_request.new_record?
.form-group
- = f.label :source_branch, class: 'control-label' do
- %i.fa.fa-code-fork
- Source Branch
+ = f.label :source_branch, class: 'control-label'
.col-sm-10
= f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
- = f.label :target_branch, class: 'control-label' do
- %i.fa.fa-code-fork
- Target Branch
+ = f.label :target_branch, class: 'control-label'
.col-sm-10
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: "Select branch" }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record? })
+ = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- if @merge_request.new_record?
%p.help-block
= link_to 'Change branches', mr_change_branches_path(@merge_request)
-.form-actions
- - if !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project)) && !issuable.persisted?
- %p
- Please review the
- %strong #{link_to 'guidelines for contribution', guide_url}
- to this repository.
+- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
+.gray-content-block{class: (is_footer ? "footer-block" : "middle-block")}
- if issuable.new_record?
- = f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
+ = f.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
- else
= f.submit 'Save changes', class: 'btn btn-save'
+
+ - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = contribution_guide_path(issuable.project))
+ .inline.prepend-left-10
+ Please review the
+ %strong #{link_to 'contribution guidelines', guide_url}
+ for this project.
+
- if issuable.new_record?
- cancel_project = issuable.source_project
- else
diff --git a/app/views/shared/issuable/_participants.html.haml b/app/views/shared/issuable/_participants.html.haml
new file mode 100644
index 00000000000..da6bacbb74a
--- /dev/null
+++ b/app/views/shared/issuable/_participants.html.haml
@@ -0,0 +1,5 @@
+.block.participants
+ .title
+ = pluralize participants.count, "participant"
+ - participants.each do |participant|
+ = link_to_member(@project, participant, name: false, size: 24)
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
new file mode 100644
index 00000000000..79c5cc7f40a
--- /dev/null
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -0,0 +1,83 @@
+.issuable-sidebar.issuable-affix
+ = form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
+ .block.assignee
+ .title
+ %label
+ Assignee
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value
+ - if issuable.assignee
+ %strong= link_to_member(@project, issuable.assignee, size: 24)
+ - else
+ .light None
+
+ .selectbox
+ = users_select_tag("#{issuable.class.table_name.singularize}[assignee_id]", placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: issuable.assignee_id, project: @target_project, null_user: true, current_user: true, first_user: true)
+
+ .block.milestone
+ .title
+ %label
+ Milestone
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value
+ - if issuable.milestone
+ %span.back-to-milestone
+ = link_to namespace_project_milestone_path(@project.namespace, @project, issuable.milestone) do
+ %strong
+ = icon('clock-o')
+ = issuable.milestone.title
+ - else
+ .light None
+ .selectbox
+ = f.select(:milestone_id, milestone_options(issuable), { include_blank: true }, { class: 'select2 select2-compact js-select2 js-milestone', data: { placeholder: 'Select milestone' }})
+ = hidden_field_tag :issuable_context
+ = f.submit class: 'btn hide'
+
+ - if issuable.project.labels.any?
+ .block
+ .title
+ %label Labels
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .pull-right
+ = link_to 'Edit', '#', class: 'edit-link'
+ .value.issuable-show-labels
+ - if issuable.labels.any?
+ - issuable.labels.each do |label|
+ = link_to_label(label)
+ - else
+ .light None
+ .selectbox
+ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
+ { selected: issuable.label_ids }, multiple: true, class: 'select2 js-select2', data: { placeholder: "Select labels" }
+
+ .block
+ .title
+ Cross-project reference
+ .cross-project-reference
+ %span#cross-project-reference
+ = cross_project_reference(@project, issuable)
+ = clipboard_button(clipboard_target: 'span#cross-project-reference')
+
+ = render "shared/issuable/participants", participants: issuable.participants(current_user)
+
+ - if current_user
+ - subscribed = issuable.subscribed?(current_user)
+ .block.light
+ .title
+ %label.light Notifications
+ - subscribtion_status = subscribed ? 'subscribed' : 'unsubscribed'
+ %button.btn.btn-block.btn-gray.subscribe-button{:type => 'button'}
+ %span= subscribed ? 'Unsubscribe' : 'Subscribe'
+ .subscription-status{data: {status: subscribtion_status}}
+ .unsubscribed{class: ( 'hidden' if subscribed )}
+ You're not receiving notifications from this thread.
+ .subscribed{class: ( 'hidden' unless subscribed )}
+ You're receiving notifications because you're subscribed to this thread.
+
+ :javascript
+ new Subscription("#{toggle_subscription_path(issuable)}");
+ new IssuableContext();
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 357cfd6a370..e5ffe1e29ae 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -17,5 +17,5 @@
= link_to '#', class: 'js-expand' do
Show all
-:coffeescript
- new ProjectsList()
+:javascript
+ new ProjectsList();
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index aee839b44e7..86249851a82 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -5,7 +5,7 @@
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" unless project.description.present?
%li.project-row{ class: css_class }
- = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do
+ = cache [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2'] do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
@@ -21,9 +21,7 @@
.project-controls
- if ci && !project.empty_repo? && project.commit
- if ci_commit = project.ci_commit(project.commit.sha)
- = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}",
- title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do
- = ci_status_icon(ci_commit)
+ = render_ci_status(ci_commit)
&nbsp;
- if stars
%span
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 913b6744844..1041eccd1df 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,5 +1,5 @@
.snippet-form-holder
- = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form" } do |f|
+ = form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
- if @snippet.errors.any?
.alert.alert-danger
%ul
@@ -8,7 +8,8 @@
.form-group
= f.label :title, class: 'control-label'
- .col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true
+ .col-sm-10
+ = f.text_field :title, class: 'form-control', required: true, autofocus: true
= render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet
@@ -27,7 +28,7 @@
- if @snippet.new_record?
= f.submit 'Create snippet', class: "btn-create btn"
- else
- = f.submit 'Save', class: "btn-save btn"
+ = f.submit 'Save changes', class: "btn-save btn"
- if @snippet.project_id
= link_to "Cancel", namespace_project_snippets_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
new file mode 100644
index 00000000000..aa5acee9c14
--- /dev/null
+++ b/app/views/shared/snippets/_header.html.haml
@@ -0,0 +1,25 @@
+.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' }}
+ = visibility_level_icon(@snippet.visibility_level, fw: false)
+ = visibility_level_label(@snippet.visibility_level)
+ %span.identifier
+ Snippet ##{@snippet.id}
+ %span.creator
+ &middot; created by #{link_to_member(@project, @snippet.author, size: 24)}
+ &middot;
+ = 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
+ - if @snippet.project_id?
+ = render "projects/snippets/actions"
+ - else
+ = render "snippets/actions"
+
+.detail-page-description.gray-content-block.second-block
+ %h2.title
+ = markdown escape_once(@snippet.title), pipeline: :single_line
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 69a713ad9aa..c6294caddc7 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -18,4 +18,3 @@
= image_tag avatar_icon(snippet.author_email), class: "avatar s24", alt: ''
= snippet.author_name
authored #{time_ago_with_tooltip(snippet.created_at)}
-
diff --git a/app/views/sherlock/file_samples/show.html.haml b/app/views/sherlock/file_samples/show.html.haml
new file mode 100644
index 00000000000..cfd11e45b6a
--- /dev/null
+++ b/app/views/sherlock/file_samples/show.html.haml
@@ -0,0 +1,55 @@
+- page_title t('sherlock.title'), t('sherlock.transaction'),
+ t('sherlock.file_sample')
+
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+.gray-content-block
+ .pull-right
+ = link_to(sherlock_transaction_path(@transaction), class: 'btn') do
+ %i.fa.fa-arrow-left
+ = t('sherlock.transaction')
+ .oneline
+ = t('sherlock.file_sample')
+ = @file_sample.id
+
+.prepend-top-default
+ %p
+ %span.light
+ #{t('sherlock.time')}:
+ %strong
+ = @file_sample.duration.round(2)
+ = t('sherlock.milliseconds')
+ %p
+ %span.light
+ #{t('sherlock.events')}:
+ %strong
+ = @file_sample.events
+
+%article.file-holder
+ .file-title
+ %i.fa.fa-file-text-o.fa-fw
+ %strong
+ = @file_sample.file
+ .code.file-content.js-syntax-highlight
+ .line-numbers
+ %table.sherlock-line-samples-table
+ %thead
+ %tr
+ %th= t('sherlock.line_capitalized')
+ %th= t('sherlock.events')
+ %th= t('sherlock.time')
+ %th= t('sherlock.percent')
+ %tbody
+ - @file_sample.line_samples.each_with_index do |sample, index|
+ %tr{class: sample.majority_of?(@file_sample.duration) ? 'slow' : ''}
+ %td= index + 1
+ %td= sample.events
+ %td
+ = sample.duration.round(2)
+ = t('sherlock.milliseconds')
+ %td
+ = sample.percentage_of(@file_sample.duration).round
+ = t('sherlock.percent')
+
+ .sherlock-file-sample
+ = highlight(@file_sample.file, @file_sample.source)
diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml
new file mode 100644
index 00000000000..5c9294c0ab5
--- /dev/null
+++ b/app/views/sherlock/queries/_backtrace.html.haml
@@ -0,0 +1,27 @@
+.prepend-top-default
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.application_backtrace')
+ %ul.well-list
+ - @query.application_backtrace.each do |location|
+ %li
+ = location.path
+ %small.light
+ = t('sherlock.line')
+ = location.line
+
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.full_backtrace')
+ %ul.well-list
+ - @query.backtrace.each do |location|
+ %li
+ - if location.application?
+ %strong= location.path
+ - 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
new file mode 100644
index 00000000000..549b47430e6
--- /dev/null
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -0,0 +1,50 @@
+.prepend-top-default
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.general')
+ %ul.well-list
+ %li
+ %span.light
+ #{t('sherlock.time')}:
+ %strong
+ = @query.duration.round(4)
+ = t('sherlock.milliseconds')
+ %li
+ %span.light
+ #{t('sherlock.origin')}:
+ %strong
+ = @query.last_application_frame.path
+ %small.light
+ = t('sherlock.line')
+ = @query.last_application_frame.line
+
+ .panel.panel-default
+ .panel-heading
+ .pull-right
+ %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %i.fa.fa-clipboard
+ %pre.hidden
+ = @query.formatted_query
+ %strong
+ = t('sherlock.query')
+ %ul.well-list
+ %li
+ .code.js-syntax-highlight.sherlock-code
+ :preserve
+ #{highlight("#{@query.id}.sql", @query.formatted_query)}
+
+ .panel.panel-default
+ .panel-heading
+ .pull-right
+ %button.js-clipboard-trigger.btn.btn-xs{title: t('sherlock.copy_to_clipboard'), type: :button}
+ %i.fa.fa-clipboard
+ %pre.hidden
+ = @query.explain
+ %strong
+ = t('sherlock.query_plan')
+ %ul.well-list
+ %li
+ .code.js-syntax-highlight.sherlock-code
+ %pre
+ %code= @query.explain
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
new file mode 100644
index 00000000000..4a84348ac82
--- /dev/null
+++ b/app/views/sherlock/queries/show.html.haml
@@ -0,0 +1,26 @@
+- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+%ul.center-top-menu
+ %li.active
+ %a(href="#tab-general" data-toggle="tab")
+ = t('sherlock.general')
+ %li
+ %a(href="#tab-backtrace" data-toggle="tab")
+ = t('sherlock.backtrace')
+
+.gray-content-block
+ .pull-right
+ = link_to(sherlock_transaction_path(@transaction), class: 'btn') do
+ %i.fa.fa-arrow-left
+ = t('sherlock.transaction')
+ .oneline
+ = t('sherlock.query')
+ = @query.id
+
+.tab-content
+ .tab-pane.active#tab-general
+ = render(partial: 'general')
+
+ .tab-pane#tab-backtrace
+ = render(partial: 'backtrace')
diff --git a/app/views/sherlock/transactions/_file_samples.html.haml b/app/views/sherlock/transactions/_file_samples.html.haml
new file mode 100644
index 00000000000..4349c9b7ace
--- /dev/null
+++ b/app/views/sherlock/transactions/_file_samples.html.haml
@@ -0,0 +1,24 @@
+- if @transaction.file_samples.empty?
+ .nothing-here-block
+ = t('sherlock.no_file_samples')
+- else
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th= t('sherlock.time_inclusive')
+ %th= t('sherlock.count')
+ %th= t('sherlock.path')
+ %th
+ %tbody
+ - @transaction.sorted_file_samples.each do |sample|
+ %tr
+ %td
+ = sample.duration.round(2)
+ = t('sherlock.milliseconds')
+ %td= @transaction.view_counts.fetch(sample.file, 1)
+ %td= sample.relative_path
+ %td
+ = link_to(t('sherlock.view'),
+ sherlock_transaction_file_sample_path(@transaction, sample),
+ class: 'btn btn-xs')
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
new file mode 100644
index 00000000000..8533b130da6
--- /dev/null
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -0,0 +1,39 @@
+.prepend-top-default
+ .panel.panel-default
+ .panel-heading
+ %strong
+ = t('sherlock.general')
+ %ul.well-list
+ %li
+ %span.light
+ #{t('sherlock.id')}:
+ %strong
+ = @transaction.id
+ %li
+ %span.light
+ #{t('sherlock.type')}:
+ %strong
+ = @transaction.type
+ %li
+ %span.light
+ #{t('sherlock.path')}:
+ %strong
+ = @transaction.path
+ %li
+ %span.light
+ #{t('sherlock.time')}:
+ %strong
+ = @transaction.duration.round(2)
+ = t('sherlock.seconds')
+ %li
+ %span.light
+ #{t('sherlock.query_time')}
+ %strong
+ = @transaction.query_duration.round(2)
+ = t('sherlock.seconds')
+ %li
+ %span.light
+ #{t('sherlock.finished_at')}:
+ %strong
+ = time_ago_in_words(@transaction.finished_at)
+ = t('sherlock.ago')
diff --git a/app/views/sherlock/transactions/_queries.html.haml b/app/views/sherlock/transactions/_queries.html.haml
new file mode 100644
index 00000000000..b7e0162e80d
--- /dev/null
+++ b/app/views/sherlock/transactions/_queries.html.haml
@@ -0,0 +1,24 @@
+- if @transaction.queries.empty?
+ .nothing-here-block
+ = t('sherlock.no_queries')
+- else
+ .table-holder
+ %table.table#sherlock-queries
+ %thead
+ %tr
+ %th= t('sherlock.time')
+ %th= t('sherlock.query')
+ %td
+ %tbody
+ - @transaction.sorted_queries.each do |query|
+ %tr
+ %td
+ = query.duration.round(2)
+ = t('sherlock.milliseconds')
+ %td
+ .code.js-syntax-highlight.sherlock-code
+ = highlight("#{query.id}.sql", query.formatted_query)
+ %td
+ = link_to(t('sherlock.view'),
+ sherlock_transaction_query_path(@transaction, query),
+ class: 'btn btn-xs')
diff --git a/app/views/sherlock/transactions/index.html.haml b/app/views/sherlock/transactions/index.html.haml
new file mode 100644
index 00000000000..010e1a2a902
--- /dev/null
+++ b/app/views/sherlock/transactions/index.html.haml
@@ -0,0 +1,42 @@
+- page_title t('sherlock.title')
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+.gray-content-block
+ .pull-right
+ = link_to(destroy_all_sherlock_transactions_path,
+ class: 'btn btn-danger',
+ method: :delete) do
+ %i.fa.fa-trash
+ = t('sherlock.delete_all_transactions')
+ .oneline= t('sherlock.introduction')
+
+- if @transactions.empty?
+ .nothing-here-block= t('sherlock.no_transactions')
+- else
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th= t('sherlock.type')
+ %th= t('sherlock.path')
+ %th= t('sherlock.time')
+ %th= t('sherlock.queries')
+ %th= t('sherlock.finished_at')
+ %th
+ %tbody
+ - @transactions.each do |trans|
+ %tr
+ %td= trans.type
+ %td
+ %span{title: trans.path}
+ = truncate(trans.path, length: 70)
+ %td
+ = trans.duration.round(2)
+ = t('sherlock.seconds')
+ %td= trans.queries.length
+ %td
+ = time_ago_in_words(trans.finished_at)
+ = t('sherlock.ago')
+ %td
+ = link_to(sherlock_transaction_path(trans), class: 'btn btn-xs') do
+ = t('sherlock.view')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
new file mode 100644
index 00000000000..3c8ffb06648
--- /dev/null
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -0,0 +1,36 @@
+- page_title t('sherlock.title'), t('sherlock.transaction')
+- header_title t('sherlock.title'), sherlock_transactions_path
+
+%ul.center-top-menu
+ %li.active
+ %a(href="#tab-general" data-toggle="tab")
+ = t('sherlock.general')
+ %li
+ %a(href="#tab-queries" data-toggle="tab")
+ = t('sherlock.queries')
+ %span.badge
+ #{@transaction.queries.length}
+ %li
+ %a(href="#tab-file-samples" data-toggle="tab")
+ = t('sherlock.file_samples')
+ %span.badge
+ #{@transaction.file_samples.length}
+
+.gray-content-block
+ .pull-right
+ = link_to(sherlock_transactions_path, class: 'btn') do
+ %i.fa.fa-arrow-left
+ = t('sherlock.all_transactions')
+ .oneline
+ = t('sherlock.transaction')
+ = @transaction.id
+
+.tab-content
+ .tab-pane.active#tab-general
+ = render(partial: 'general')
+
+ .tab-pane#tab-queries
+ = render(partial: 'queries')
+
+ .tab-pane#tab-file-samples
+ = render(partial: 'file_samples')
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
new file mode 100644
index 00000000000..1979ae6d5bc
--- /dev/null
+++ b/app/views/snippets/_actions.html.haml
@@ -0,0 +1,11 @@
+= 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
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 1a380035661..82f44a9a5c3 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,5 +1,5 @@
- page_title "Edit", @snippet.title, "Snippets"
%h3.page-title
- Edit snippet
+ Edit Snippet
%hr
= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index a74d5e792ad..79e2392490d 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -1,5 +1,5 @@
- page_title "New Snippet"
%h3.page-title
- New snippet
+ New Snippet
%hr
= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 97374e073dc..a2b36568770 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,41 +1,14 @@
- page_title @snippet.title, "Snippets"
-%h4.page-title
- = @snippet.title
- - if @snippet.private?
- %span.label.label-success
- %i.fa.fa-lock
- private
-
- .pull-right
- = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do
- Add new snippet
-
-.append-bottom-10.prepend-top-10
- .pull-right
- %span.light
- created by
- = link_to user_snippets_path(@snippet.author) do
- = @snippet.author_name
-
- .back-link
- - if @snippet.author == current_user
- = link_to dashboard_snippets_path do
- &larr; your snippets
- - else
- = link_to explore_snippets_path do
- &larr; explore snippets
-
-.file-holder
- .file-title
- %i.fa.fa-file
- %strong
- = @snippet.file_name
- .file-actions
- .btn-group
- - if can?(current_user, :update_personal_snippet, @snippet)
- = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet'
- = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- - if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
- = render 'shared/snippets/blob'
+.snippet-holder
+ = render 'shared/snippets/header'
+
+ %article.file-holder
+ .file-title
+ = blob_icon 0, @snippet.file_name
+ %strong
+ = @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"
+ = render 'shared/snippets/blob'
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 50232dc7186..2fe5b7fac83 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user)
- xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 4ea4a1f92c2..0bca8177e14 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,5 +1,6 @@
-- page_title @user.name
-- header_title @user.name, user_path(@user)
+- page_title @user.name
+- page_description @user.bio
+- header_title @user.name, user_path(@user)
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
@@ -24,22 +25,27 @@
.cover-desc
- unless @user.public_email.blank?
- = link_to @user.public_email, "mailto:#{@user.public_email}"
+ .profile-link-holder
+ = link_to @user.public_email, "mailto:#{@user.public_email}"
- unless @user.skype.blank?
- &middot;
- = link_to "Skype", "skype:#{@user.skype}"
+ .profile-link-holder
+ = link_to "skype:#{@user.skype}", title: "Skype" do
+ = icon('skype')
- unless @user.linkedin.blank?
- &middot;
- = link_to "LinkedIn", "http://www.linkedin.com/in/#{@user.linkedin}"
+ .profile-link-holder
+ = link_to "https://www.linkedin.com/in/#{@user.linkedin}", title: "LinkedIn" do
+ = icon('linkedin-square')
- unless @user.twitter.blank?
- &middot;
- = link_to "Twitter", "http://www.twitter.com/#{@user.twitter}"
+ .profile-link-holder
+ = link_to "https://twitter.com/#{@user.twitter}", title: "Twitter" do
+ = icon('twitter-square')
- unless @user.website_url.blank?
- &middot;
- = link_to @user.short_website_url, @user.full_website_url
+ .profile-link-holder
+ = link_to @user.short_website_url, @user.full_website_url
- unless @user.location.blank?
- &middot;
- = @user.location
+ .profile-link-holder
+ = icon('map-marker')
+ = @user.location
.cover-controls
@@ -47,7 +53,7 @@
= link_to profile_path, class: 'btn btn-gray' do
= icon('pencil')
- elsif current_user
- .report-abuse
+ %span.report-abuse
- if @user.abuse_report
%button.btn.btn-danger{ title: 'Already reported for abuse',
data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
@@ -56,6 +62,10 @@
= link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
= icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
+ = icon('rss')
.gray-content-block.second-block
.user-calendar
@@ -64,27 +74,47 @@
.user-calendar-activities
-.row.prepend-top-20
- %section.col-md-7
- - if @groups.any?
- .prepend-top-20
- %h4 Groups
- = render 'groups', groups: @groups
- %hr
-
- %h4
- User Activity
-
- - if current_user
- %span.rss-icon.pull-right
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }) do
- %strong
- %i.fa.fa-rss
+%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
+ %li.active
+ = link_to "#activity", 'data-toggle' => 'tab' do
+ Activity
+ - if @groups.any?
+ %li
+ = link_to "#groups", 'data-toggle' => 'tab' do
+ Groups
+ - if @contributed_projects.present?
+ %li
+ = link_to "#contributed", 'data-toggle' => 'tab' do
+ Contributed projects
+ - if @projects.present?
+ %li
+ = link_to "#personal", 'data-toggle' => 'tab' do
+ Personal projects
+.tab-content
+ .tab-pane.active#activity
.content_list
= spinner
- %aside.col-md-5
- = render 'projects', projects: @projects, contributed_projects: @contributed_projects
-:coffeescript
- $(".user-calendar").load("#{user_calendar_path}")
+ - if @groups.any?
+ .tab-pane#groups
+ %ul.content-list
+ - @groups.each do |group|
+ = render 'shared/groups/group', group: group
+
+ - if @contributed_projects.present?
+ .tab-pane#contributed
+ .contributed-projects
+ = render 'shared/projects/list',
+ projects: @contributed_projects.sort_by(&:star_count).reverse,
+ projects_limit: 5, stars: true, avatar: true
+
+ - if @projects.present?
+ .tab-pane#personal
+ .personal-projects
+ = render 'shared/projects/list',
+ projects: @projects.sort_by(&:star_count).reverse,
+ projects_limit: 10, stars: true, avatar: true
+
+:javascript
+ $(".user-calendar").load("#{user_calendar_path}");
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 36ea6742064..ce0a0113403 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,10 +1,46 @@
-.votes.votes-block
- .btn-group
- - unless votable.upvotes.zero?
- .btn.btn-sm.disabled.cgreen
- %i.fa.fa-thumbs-up
- = votable.upvotes
- - unless votable.downvotes.zero?
- .btn.btn-sm.disabled.cred
- %i.fa.fa-thumbs-down
- = votable.downvotes
+.awards.votes-block
+ - awards_sort(votable.notes.awards.grouped_awards).each do |emoji, notes|
+ .award{class: (note_active_class(notes, current_user)), title: emoji_author_list(notes, current_user)}
+ = emoji_icon(emoji)
+ .counter
+ = notes.count
+
+ - if current_user
+ .awards-controls
+ %a.add-award{"data-toggle" => "dropdown", "data-target" => "#", "href" => "#"}
+ = icon('smile-o')
+ .emoji-menu
+ .emoji-menu-content
+ = text_field_tag :emoji_search, "", class: "emoji-search search-input form-control"
+ - AwardEmoji.emoji_by_category.each do |category, emojis|
+ %h5= AwardEmoji::CATEGORIES[category]
+ %ul
+ - emojis.each do |emoji|
+ %li
+ = emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
+
+- if current_user
+ :coffeescript
+ post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}"
+ noteable_type = "#{votable.class.name.underscore}"
+ noteable_id = "#{votable.id}"
+ aliases = #{AwardEmoji.aliases.to_json}
+
+ window.awards_handler = new AwardsHandler(
+ post_emoji_url,
+ noteable_type,
+ noteable_id,
+ aliases
+ )
+
+ $(".awards").on "click", ".emoji-menu-content li", (e) ->
+ emoji = $(this).find(".emoji-icon").data("emoji")
+ awards_handler.addAward(emoji)
+
+ $(".awards").on "click", ".award", (e) ->
+ emoji = $(this).find(".icon").data("emoji")
+ awards_handler.addAward(emoji)
+
+ $(".award").tooltip()
+
+ $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false})
diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml
deleted file mode 100644
index 2cb3ae04e1a..00000000000
--- a/app/views/votes/_votes_inline.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.votes.votes-inline
- - unless votable.upvotes.zero?
- %span.upvotes.cgreen
- + #{votable.upvotes}
- - unless votable.downvotes.zero?
- \/
- - unless votable.downvotes.zero?
- %span.downvotes.cred
- \- #{votable.downvotes}
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
new file mode 100644
index 00000000000..1c7a04a66a8
--- /dev/null
+++ b/app/workers/build_email_worker.rb
@@ -0,0 +1,19 @@
+class BuildEmailWorker
+ include Sidekiq::Worker
+
+ def perform(build_id, recipients, push_data)
+ recipients.each do |recipient|
+ begin
+ case push_data['build_status']
+ when 'success'
+ Notify.build_success_email(build_id, recipient).deliver_now
+ when 'failed'
+ Notify.build_fail_email(build_id, recipient).deliver_now
+ end
+ # 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 '#{push_data['project_name']}' to #{recipient}: #{e}")
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/hip_chat_notifier_worker.rb b/app/workers/ci/hip_chat_notifier_worker.rb
deleted file mode 100644
index ebb43570e2a..00000000000
--- a/app/workers/ci/hip_chat_notifier_worker.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Ci
- class HipChatNotifierWorker
- include Sidekiq::Worker
-
- def perform(message, options={})
- room = options.delete('room')
- token = options.delete('token')
- server = options.delete('server')
- name = options.delete('service_name')
- client_opts = {
- api_version: 'v2',
- server_url: server
- }
-
- client = HipChat::Client.new(token, client_opts)
- client[room].send(name, message, options.symbolize_keys)
- end
- end
-end
diff --git a/app/workers/ci/slack_notifier_worker.rb b/app/workers/ci/slack_notifier_worker.rb
deleted file mode 100644
index 3bbb9b4bec7..00000000000
--- a/app/workers/ci/slack_notifier_worker.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module Ci
- class SlackNotifierWorker
- include Sidekiq::Worker
-
- def perform(webhook_url, message, options={})
- notifier = Slack::Notifier.new(webhook_url)
- notifier.ping(message, options)
- end
- end
-end
diff --git a/app/workers/ci/web_hook_worker.rb b/app/workers/ci/web_hook_worker.rb
deleted file mode 100644
index 0bb83845572..00000000000
--- a/app/workers/ci/web_hook_worker.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-module Ci
- class WebHookWorker
- include Sidekiq::Worker
-
- def perform(hook_id, data)
- Ci::WebHook.find(hook_id).execute data
- end
- end
-end
diff --git a/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 5a921a73fe9..f2649e38eb3 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -46,6 +46,6 @@ class EmailReceiverWorker
return
end
- EmailRejectionMailer.delay.rejection(reason, raw, can_retry)
+ EmailRejectionMailer.rejection(reason, raw, can_retry).deliver_later
end
end
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 916a99bb273..c4d8595d45d 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -53,7 +53,7 @@ class EmailsOnPushWorker
reverse_compare: reverse_compare,
send_from_committer_email: send_from_committer_email,
disable_diffs: disable_diffs
- ).deliver
+ ).deliver_now
# 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}")
diff --git a/app/workers/merge_worker.rb b/app/workers/merge_worker.rb
index 5d1a8555b7d..c87c0a252b1 100644
--- a/app/workers/merge_worker.rb
+++ b/app/workers/merge_worker.rb
@@ -8,16 +8,7 @@ class MergeWorker
current_user = User.find(current_user_id)
merge_request = MergeRequest.find(merge_request_id)
- result = MergeRequests::MergeService.new(merge_request.target_project, current_user).
- execute(merge_request, params[:commit_message])
-
- if result[:status] == :success && params[:should_remove_source_branch].present?
- DeleteBranchService.new(merge_request.source_project, current_user).
- execute(merge_request.source_branch)
-
- merge_request.source_project.repository.expire_branch_names
- end
-
- result
+ MergeRequests::MergeService.new(merge_request.target_project, current_user, params).
+ execute(merge_request)
end
end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index acd1c43f06b..2f991c52339 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -13,22 +13,20 @@ class RepositoryForkWorker
end
result = gitlab_shell.fork_repository(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.save
return
end
- if project.valid_repo?
- ProjectCacheWorker.perform_async(project.id)
- project.import_finish
- else
- project.import_fail
+ unless project.valid_repo?
logger.error("Project #{id} had an invalid repository after fork")
+ project.update(import_error: "The forked repository is invalid.")
+ project.import_fail
+ return
end
- project.save
+ project.import_finish
end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index ea2808045eb..d18c0706b30 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -7,37 +7,49 @@ class RepositoryImportWorker
def perform(project_id)
project = Project.find(project_id)
- unless project.import_url == Project::UNKNOWN_IMPORT_URL
- import_result = gitlab_shell.send(:import_repository,
- project.path_with_namespace,
- project.import_url)
- return project.import_fail unless import_result
- else
+ if project.import_url == Project::UNKNOWN_IMPORT_URL
+ # In this case, we only want to import issues, not a repository.
unless project.create_repository
- return project.import_fail
+ project.update(import_error: "The repository could not be created.")
+ project.import_fail
+ return
+ end
+ else
+ begin
+ gitlab_shell.import_repository(project.path_with_namespace, project.import_url)
+ rescue Gitlab::Shell::Error => e
+ project.update(import_error: e.message)
+ project.import_fail
+ return
end
end
- data_import_result = case project.import_type
- when 'github'
- Gitlab::GithubImport::Importer.new(project).execute
- when 'gitlab'
- Gitlab::GitlabImport::Importer.new(project).execute
- when 'bitbucket'
- Gitlab::BitbucketImport::Importer.new(project).execute
- when 'google_code'
- Gitlab::GoogleCodeImport::Importer.new(project).execute
- when 'fogbugz'
- Gitlab::FogbugzImport::Importer.new(project).execute
- else
- true
- end
- return project.import_fail unless data_import_result
+ data_import_result =
+ case project.import_type
+ when 'github'
+ Gitlab::GithubImport::Importer.new(project).execute
+ when 'gitlab'
+ Gitlab::GitlabImport::Importer.new(project).execute
+ when 'bitbucket'
+ Gitlab::BitbucketImport::Importer.new(project).execute
+ when 'google_code'
+ Gitlab::GoogleCodeImport::Importer.new(project).execute
+ when 'fogbugz'
+ Gitlab::FogbugzImport::Importer.new(project).execute
+ else
+ true
+ end
- Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket'
+ unless data_import_result
+ project.update(import_error: "The remote issue data could not be imported.")
+ project.import_fail
+ return
+ end
+
+ if project.import_type == 'bitbucket'
+ Gitlab::BitbucketImport::KeyDeleter.new(project).execute
+ end
project.import_finish
- project.save
- ProjectCacheWorker.perform_async(project.id)
end
end
diff --git a/app/workers/stuck_ci_builds_worker.rb b/app/workers/stuck_ci_builds_worker.rb
new file mode 100644
index 00000000000..ca594e77e7c
--- /dev/null
+++ b/app/workers/stuck_ci_builds_worker.rb
@@ -0,0 +1,18 @@
+class StuckCiBuildsWorker
+ include Sidekiq::Worker
+
+ BUILD_STUCK_TIMEOUT = 1.day
+
+ def perform
+ Rails.logger.info 'Cleaning stuck builds'
+
+ builds = Ci::Build.running_or_pending.where('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
+ end
+
+ # Update builds that failed to drop
+ builds.update_all(status: 'failed')
+ end
+end
diff --git a/bin/background_jobs b/bin/background_jobs
index d4578f6a222..5c85fb339e6 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -37,7 +37,7 @@ start_no_deamonize()
start_sidekiq()
{
- bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
+ bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
}
load_ok()
diff --git a/bin/ci/upgrade.rb b/bin/ci/upgrade.rb
index aab4f60ec60..aab4f60ec60 100644..100755
--- a/bin/ci/upgrade.rb
+++ b/bin/ci/upgrade.rb
diff --git a/bin/parallel-rsync-repos b/bin/parallel-rsync-repos
new file mode 100755
index 00000000000..21921148fa0
--- /dev/null
+++ b/bin/parallel-rsync-repos
@@ -0,0 +1,54 @@
+#!/usr/bin/env bash
+# this script should run as the 'git' user, not root, because 'root' should not
+# own intermediate directories created by rsync.
+#
+# Example invocation:
+# find /var/opt/gitlab/git-data/repositories -maxdepth 2 | \
+# parallel-rsync-repos transfer-success.log /var/opt/gitlab/git-data/repositories /mnt/gitlab/repositories
+#
+# You can also rsync to a remote destination.
+#
+# parallel-rsync-repos transfer-success.log /var/opt/gitlab/git-data/repositories user@host:/mnt/gitlab/repositories
+#
+# If you need to pass extra options to rsync, set the RSYNC variable
+#
+# env RSYNC='rsync --rsh="foo bar"' parallel-rsync-repos transfer-success.log /src dest
+#
+
+LOGFILE=$1
+SRC=$2
+DEST=$3
+
+if [ -z "$LOGFILE" ] || [ -z "$SRC" ] || [ -z "$DEST" ] ; then
+ echo "Usage: $0 LOGFILE SRC DEST"
+ exit 1
+fi
+
+if [ -z "$JOBS" ] ; then
+ JOBS=10
+fi
+
+if [ -z "$RSYNC" ] ; then
+ RSYNC=rsync
+fi
+
+if ! cd $SRC ; then
+ echo "cd $SRC failed"
+ exit 1
+fi
+
+rsyncjob() {
+ relative_dir="./${1#$SRC}"
+
+ if ! $RSYNC --delete --relative -a "$relative_dir" "$DEST" ; then
+ echo "rsync $1 failed"
+ return 1
+ fi
+
+ echo "$1" >> $LOGFILE
+}
+
+export LOGFILE SRC DEST RSYNC
+export -f rsyncjob
+
+parallel -j$JOBS --progress rsyncjob
diff --git a/bin/rails b/bin/rails
index 7feb6a30e69..5191e6927af 100755
--- a/bin/rails
+++ b/bin/rails
@@ -1,8 +1,4 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path("../spring", __FILE__)
-rescue LoadError
-end
-APP_PATH = File.expand_path('../../config/application', __FILE__)
+APP_PATH = File.expand_path('../../config/application', __FILE__)
require_relative '../config/boot'
require 'rails/commands'
diff --git a/bin/rake b/bin/rake
index 0fb4e07e13a..17240489f64 100755
--- a/bin/rake
+++ b/bin/rake
@@ -1,7 +1,4 @@
#!/usr/bin/env ruby
-begin
- load File.expand_path("../spring", __FILE__)
-rescue LoadError
-end
-require 'bundler/setup'
-load Gem.bin_path('rake', 'rake')
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/bin/setup b/bin/setup
new file mode 100755
index 00000000000..acdb2c1389c
--- /dev/null
+++ b/bin/setup
@@ -0,0 +1,29 @@
+#!/usr/bin/env ruby
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
diff --git a/bin/upgrade.rb b/bin/upgrade.rb
index a5caecf8526..a5caecf8526 100644..100755
--- a/bin/upgrade.rb
+++ b/bin/upgrade.rb
diff --git a/config/application.rb b/config/application.rb
index bfa2a809dd7..d255ff0719f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -99,6 +99,10 @@ module Gitlab
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
config.cache_store = :redis_store, redis_config_hash
+ config.active_record.raise_in_transactional_callbacks = true
+
+ config.active_job.queue_adapter = :sidekiq
+
# This is needed for gitlab-shell
ENV['GITLAB_PATH_OUTSIDE_HOOK'] = ENV['PATH']
end
diff --git a/config/database.yml.env b/config/database.yml.env
new file mode 100644
index 00000000000..b2ff23cb5ab
--- /dev/null
+++ b/config/database.yml.env
@@ -0,0 +1,9 @@
+<%= ENV['RAILS_ENV'] %>:
+ adapter: <%= ENV['GITLAB_DATABASE_ADAPTER'] || 'postgresql' %>
+ encoding: <%= ENV['GITLAB_DATABASE_ENCODING'] || 'unicode' %>
+ database: <%= ENV['GITLAB_DATABASE_DATABASE'] || "gitlab_#{ENV['RAILS_ENV']}" %>
+ pool: <%= ENV['GITLAB_DATABASE_POOL'] || '10' %>
+ username: <%= ENV['GITLAB_DATABASE_USERNAME'] || 'root' %>
+ password: <%= ENV['GITLAB_DATABASE_PASSWORD'] || '' %>
+ host: <%= ENV['GITLAB_DATABASE_HOST'] || 'localhost' %>
+ port: <%= ENV['GITLAB_DATABASE_PORT'] || '5432' %>
diff --git a/config/environment.rb b/config/environment.rb
index 3b186a9d57a..df3006d349c 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -2,4 +2,4 @@
require File.expand_path('../application', __FILE__)
# Initialize the rails application
-Gitlab::Application.initialize!
+Rails.application.initialize!
diff --git a/config/environments/development.rb b/config/environments/development.rb
index 827a110c249..c22722c606b 100644
--- a/config/environments/development.rb
+++ b/config/environments/development.rb
@@ -1,4 +1,4 @@
-Gitlab::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb
# In the development environment your application's code is reloaded on
diff --git a/config/environments/production.rb b/config/environments/production.rb
index 3316ece3873..909526605a1 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -1,4 +1,4 @@
-Gitlab::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb
# Code is not reloaded between requests
@@ -9,7 +9,7 @@ Gitlab::Application.configure do
config.action_controller.perform_caching = true
# Disable Rails's static asset server (Apache or nginx will already do this)
- config.serve_static_assets = false
+ config.serve_static_files = false
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
@@ -32,7 +32,7 @@ Gitlab::Application.configure do
# config.force_ssl = true
# See everything in the log (default is :info)
- # config.log_level = :debug
+ config.log_level = :info
# Suppress 'Rendered template ...' messages in the log
# source: http://stackoverflow.com/a/16369363
diff --git a/config/environments/test.rb b/config/environments/test.rb
index 2d5e7addcd3..d6842affa6c 100644
--- a/config/environments/test.rb
+++ b/config/environments/test.rb
@@ -1,4 +1,4 @@
-Gitlab::Application.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb
# The test environment is used exclusively to run your application's
@@ -7,8 +7,10 @@ Gitlab::Application.configure do
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = false
+ config.cache_store = :null_store
+
# Configure static asset server for tests with Cache-Control for performance
- config.serve_static_assets = true
+ config.serve_static_files = true
config.static_cache_control = "public, max-age=3600"
# Show full error reports and disable caching
@@ -30,4 +32,8 @@ Gitlab::Application.configure do
config.active_support.deprecation = :stderr
config.eager_load = false
+
+ config.cache_store = :null_store
+
+ config.active_job.queue_adapter = :test
end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 8b85981497a..2d9f730c183 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -4,8 +4,8 @@
#
########################### NOTE #####################################
# This file should not receive new settings. All configuration options #
-# that do not require an application restart are being moved to #
-# ApplicationSetting model! #
+# * are being moved to ApplicationSetting model! #
+# If a setting requires an application restart say so in that screen. #
# If you change this file in a Merge Request, please also create #
# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests #
########################################################################
@@ -76,7 +76,7 @@ production: &base
# This happens when the commit is pushed or merged into the default branch of a project.
# When not specified the default issue_closing_pattern as specified below will be used.
# Tip: you can test your closing pattern at http://rubular.com.
- # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)'
+ # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
## Default project features settings
default_projects_features:
@@ -84,6 +84,7 @@ production: &base
merge_requests: true
wiki: true
snippets: false
+ builds: true
## Webhook settings
# Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10)
@@ -123,6 +124,18 @@ production: &base
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ ## Build Artifacts
+ artifacts:
+ enabled: true
+ # The location where build artifacts are stored (default: shared/artifacts).
+ # path: shared/artifacts
+
+ ## Git LFS
+ lfs:
+ enabled: true
+ # The location where LFS objects are stored (default: shared/lfs-objects).
+ # storage_path: shared/lfs-objects
+
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
@@ -131,6 +144,15 @@ production: &base
# plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
+ ## Auxiliary jobs
+ # Periodically executed jobs, to self-heal Gitlab, do external synchronizations, etc.
+ # Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
+ cron_jobs:
+ # Flag stuck CI builds as failed
+ stuck_ci_builds_worker:
+ cron: "0 0 * * *"
+
+
#
# 2. GitLab CI settings
# ==========================
@@ -274,27 +296,37 @@ production: &base
# arguments, followed by optional 'args' which can be either a hash or an array.
# Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html
providers:
- # - { name: 'google_oauth2',
- # label: 'Google',
- # app_id: 'YOUR_APP_ID',
- # app_secret: 'YOUR_APP_SECRET',
- # args: { access_type: 'offline', approval_prompt: '' } }
- # - { name: 'twitter',
- # app_id: 'YOUR_APP_ID',
- # app_secret: 'YOUR_APP_SECRET' }
+ # See omniauth-cas3 for more configuration details
+ # - { name: 'cas3',
+ # label: 'cas3',
+ # args: {
+ # url: 'https://sso.example.com',
+ # disable_ssl_verification: false,
+ # login_url: '/cas/login',
+ # service_validate_url: '/cas/p3/serviceValidate',
+ # logout_url: '/cas/logout'} }
# - { name: 'github',
- # label: 'GitHub',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'user:email' } }
+ # - { name: 'bitbucket',
+ # app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET' }
# - { name: 'gitlab',
- # label: 'GitLab.com',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'api' } }
- # - { name: 'bitbucket',
+ # - { name: 'google_oauth2',
+ # app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET',
+ # args: { access_type: 'offline', approval_prompt: '' } }
+ # - { name: 'facebook',
+ # app_id: 'YOUR_APP_ID',
+ # app_secret: 'YOUR_APP_SECRET' }
+ # - { name: 'twitter',
# app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
+ #
# - { name: 'saml',
# label: 'Our SAML Provider',
# args: {
@@ -310,7 +342,13 @@ production: &base
# application_name: 'YOUR_APP_NAME',
# application_password: 'YOUR_APP_PASSWORD' } }
+ # SSO maximum session duration in seconds. Defaults to CAS default of 8 hours.
+ # cas3:
+ # session_duration: 28800
+ # Shared file storage settings
+ shared:
+ # path: /mnt/gitlab # Default: shared
#
@@ -318,10 +356,12 @@ production: &base
# ==========================
# GitLab Satellites
+ #
+ # Note for maintainers: keep the satellites.path setting until GitLab 9.0 at
+ # least. This setting is fed to 'rm -rf' in
+ # db/migrate/20151023144219_remove_satellites.rb
satellites:
- # Relative paths are relative to Rails.root (default: tmp/repo_satellites/)
path: /home/git/gitlab-satellites/
- timeout: 30
## Backup settings
backup:
@@ -411,6 +451,8 @@ test:
<<: *base
gravatar:
enabled: true
+ lfs:
+ enabled: false
gitlab:
host: localhost
port: 80
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d5493ca038d..4fbd84ee890 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -33,13 +33,15 @@ class Settings < Settingslogic
end
def build_gitlab_shell_ssh_path_prefix
+ user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
+
if gitlab_shell.ssh_port != 22
- "ssh://#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:#{gitlab_shell.ssh_port}/"
+ "ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
- "[#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}]:"
+ "[#{user_host}]:"
else
- "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}:"
+ "#{user_host}:"
end
end
end
@@ -124,6 +126,14 @@ 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['providers'] ||= []
+Settings.omniauth['cas3'] ||= Settingslogic.new({})
+Settings.omniauth.cas3['session_duration'] ||= 8.hours
+Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
+Settings.omniauth.session_tickets['cas3'] = 'ticket'
+
+
+Settings['shared'] ||= Settingslogic.new({})
+Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
Settings['issues_tracker'] ||= {}
@@ -135,16 +145,16 @@ Settings.gitlab['default_projects_limit'] ||= 10
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
-Settings.gitlab['host'] ||= 'localhost'
+Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
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['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
-Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}"
-Settings.gitlab['email_display_name'] ||= "GitLab"
-Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}"
+Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
+Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
+Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url)
Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
@@ -159,7 +169,7 @@ Settings.gitlab['signin_enabled'] ||= true if Settings.gitlab['signin_enabled'].
Settings.gitlab['twitter_sharing_enabled'] ||= true if Settings.gitlab['twitter_sharing_enabled'].nil?
Settings.gitlab['restricted_visibility_levels'] = Settings.send(:verify_constant_array, Gitlab::VisibilityLevel, Settings.gitlab['restricted_visibility_levels'], [])
Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username_changing_enabled'].nil?
-Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil?
+Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?)|([A-Z]*-\d*))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
Settings.gitlab['max_attachment_size'] ||= 10
@@ -168,8 +178,9 @@ Settings.gitlab.default_projects_features['issues'] = true if Settings.g
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['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root)
+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']
@@ -178,10 +189,11 @@ Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious'
# CI
#
Settings['gitlab_ci'] ||= Settingslogic.new({})
-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['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)
#
# Reply by email
@@ -189,11 +201,26 @@ Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci[
Settings['incoming_email'] ||= Settingslogic.new({})
Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
Settings.incoming_email['port'] = 143 if Settings.incoming_email['port'].nil?
-Settings.incoming_email['ssl'] = 143 if Settings.incoming_email['ssl'].nil?
-Settings.incoming_email['start_tls'] = 143 if Settings.incoming_email['start_tls'].nil?
+Settings.incoming_email['ssl'] = false if Settings.incoming_email['ssl'].nil?
+Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_tls'].nil?
Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil?
#
+# Build Artifacts
+#
+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
+
+#
+# Git LFS
+#
+Settings['lfs'] ||= Settingslogic.new({})
+Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
+Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
+
+#
# Gravatar
#
Settings['gravatar'] ||= Settingslogic.new({})
@@ -203,6 +230,15 @@ Settings.gravatar['ssl_url'] ||= 'https://secure.gravatar.com/avatar/%{hash}?
Settings.gravatar['host'] = Settings.get_host_without_www(Settings.gravatar['plain_url'])
#
+# Cron Jobs
+#
+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'
+
+
+#
# GitLab Shell
#
Settings['gitlab_shell'] ||= Settingslogic.new({})
@@ -242,9 +278,12 @@ Settings.git['max_size'] ||= 20971520 # 20.megabytes
Settings.git['bin_path'] ||= '/usr/bin/git'
Settings.git['timeout'] ||= 10
+# Important: keep the satellites.path setting until GitLab 9.0 at
+# least. This setting is fed to 'rm -rf' in
+# db/migrate/20151023144219_remove_satellites.rb
Settings['satellites'] ||= Settingslogic.new({})
Settings.satellites['path'] = File.expand_path(Settings.satellites['path'] || "tmp/repo_satellites/", Rails.root)
-Settings.satellites['timeout'] ||= 30
+
#
# Extra customization
@@ -270,3 +309,12 @@ if Rails.env.test?
Settings.gitlab['default_can_create_group'] = true
Settings.gitlab['default_can_create_team'] = false
end
+
+# Force a refresh of application settings at startup
+begin
+ ApplicationSetting.expire
+ Ci::ApplicationSetting.expire
+rescue
+ # Gracefully handle when Redis is not available. For example,
+ # omnibus may fail here during assets:precompile.
+end
diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb
index 688cdf5f4b0..35b150c9929 100644
--- a/config/initializers/2_app.rb
+++ b/config/initializers/2_app.rb
@@ -1,8 +1,8 @@
module Gitlab
- VERSION = File.read(Rails.root.join("VERSION")).strip
- REVISION = Gitlab::Popen.popen(%W(git log --pretty=format:%h -n 1)).first.chomp
-
def self.config
Settings
end
+
+ VERSION = File.read(Rails.root.join("VERSION")).strip
+ REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp
end
diff --git a/config/initializers/4_ci_app.rb b/config/initializers/4_ci_app.rb
index cac8edb32bf..d252e403102 100644
--- a/config/initializers/4_ci_app.rb
+++ b/config/initializers/4_ci_app.rb
@@ -1,8 +1,6 @@
module GitlabCi
VERSION = Gitlab::VERSION
REVISION = Gitlab::REVISION
-
- REGISTRATION_TOKEN = SecureRandom.hex(10)
def self.config
Settings
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index bfb8656df55..df28d30d750 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -31,11 +31,11 @@ if File.exists?(aws_file)
if Rails.env.test?
Fog.mock!
connection = ::Fog::Storage.new(
- aws_access_key_id: AWS_CONFIG['access_key_id'],
- aws_secret_access_key: AWS_CONFIG['secret_access_key'],
- provider: 'AWS',
- region: AWS_CONFIG['region']
- )
+ aws_access_key_id: AWS_CONFIG['access_key_id'],
+ aws_secret_access_key: AWS_CONFIG['secret_access_key'],
+ provider: 'AWS',
+ region: AWS_CONFIG['region']
+ )
connection.directories.create(key: AWS_CONFIG['bucket'])
end
end
diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb
index 43adac8b2c6..54516e3f23d 100644
--- a/config/initializers/cookies_serializer.rb
+++ b/config/initializers/cookies_serializer.rb
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-Gitlab::Application.config.action_dispatch.cookies_serializer = :hybrid
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
diff --git a/config/initializers/default_url_options.rb b/config/initializers/default_url_options.rb
index f9f88f95db9..8fd27b1d88e 100644
--- a/config/initializers/default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -8,4 +8,4 @@ unless Gitlab.config.gitlab_on_standard_port?
default_url_options[:port] = Gitlab.config.gitlab.port
end
-Gitlab::Application.routes.default_url_options = default_url_options
+Rails.application.routes.default_url_options = default_url_options
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 29506970af2..d82cfb3ec0c 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -60,7 +60,7 @@ Devise.setup do |config|
# It will change confirmation, password recovery and other workflows
# to behave the same regardless if the e-mail provided was right or wrong.
# Does not affect registerable.
- # config.paranoid = true
+ config.paranoid = true
# ==> Configuration for :database_authenticatable
# For bcrypt, this is the cost for hashing the password and defaults to 10. If
@@ -121,14 +121,14 @@ Devise.setup do |config|
config.lock_strategy = :failed_attempts
# Defines which key will be used when locking and unlocking an account
- # config.unlock_keys = [ :email ]
+ config.unlock_keys = [ :email ]
# Defines which strategy will be used to unlock an account.
# :email = Sends an unlock link to the user email
# :time = Re-enables login after a certain amount of time (see :unlock_in below)
# :both = Enables both strategies
# :none = No unlock strategy. You should handle unlocking by yourself.
- config.unlock_strategy = :time
+ config.unlock_strategy = :both
# Number of authentication tries before locking an account if lock_strategy
# is failed attempts.
@@ -241,6 +241,16 @@ Devise.setup do |config|
# An Array from the configuration will be expanded.
provider_arguments.concat provider['args']
when Hash
+ # Add procs for handling SLO
+ if provider['name'] == 'cas3'
+ 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)
+ true
+ end
+ end
+
# A Hash from the configuration will be passed as is.
provider_arguments << provider['args'].symbolize_keys
end
diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb
index 5d46ece1e1b..9e8b0131f8f 100644
--- a/config/initializers/inflections.rb
+++ b/config/initializers/inflections.rb
@@ -8,24 +8,3 @@
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
-
-# Mark "commits" as uncountable.
-#
-# Without this change, the routes
-#
-# resources :commit, only: [:show], constraints: {id: /[[:alnum:]]{6,40}/}
-# resources :commits, only: [:show], constraints: {id: /.+/}
-#
-# would generate identical route helper methods (`project_commit_path`), resulting
-# in one of them not getting a helper method at all.
-#
-# After this change, the helper methods are:
-#
-# project_commit_path(@project, @project.commit)
-# # => "/gitlabhq/commit/bcf03b5de6c33f3869ef70d68cf06e679d1d7f9a
-#
-# project_commits_path(@project, 'stable/README.md')
-# # => "/gitlabhq/commits/stable/README.md"
-ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable %w(commits)
-end
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
new file mode 100644
index 00000000000..52ace27b7ae
--- /dev/null
+++ b/config/initializers/metrics.rb
@@ -0,0 +1,63 @@
+if Gitlab::Metrics.enabled?
+ require 'influxdb'
+ require 'connection_pool'
+ require 'method_source'
+
+ # These are manually require'd so the classes are registered properly with
+ # ActiveSupport.
+ require 'gitlab/metrics/subscribers/action_view'
+ require 'gitlab/metrics/subscribers/active_record'
+
+ Gitlab::Application.configure do |config|
+ config.middleware.use(Gitlab::Metrics::RackMiddleware)
+ end
+
+ Sidekiq.configure_server do |config|
+ config.server_middleware do |chain|
+ chain.add Gitlab::Metrics::SidekiqMiddleware
+ end
+ end
+
+ # This instruments all methods residing in app/models that (appear to) use any
+ # of the ActiveRecord methods. This has to take place _after_ initializing as
+ # for some unknown reason calling eager_load! earlier breaks Devise.
+ Gitlab::Application.config.after_initialize do
+ Rails.application.eager_load!
+
+ models = Rails.root.join('app', 'models').to_s
+
+ regex = Regexp.union(
+ ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
+ )
+
+ Gitlab::Metrics::Instrumentation.
+ instrument_class_hierarchy(ActiveRecord::Base) do |klass, method|
+ # Instrumenting the ApplicationSetting class can lead to an infinite
+ # loop. Since the data is cached any way we don't really need to
+ # instrument it.
+ if klass == ApplicationSetting
+ false
+ else
+ loc = method.source_location
+
+ loc && loc[0].start_with?(models) && method.source =~ regex
+ end
+ end
+ end
+
+ Gitlab::Metrics::Instrumentation.configure do |config|
+ config.instrument_instance_methods(Gitlab::Shell)
+
+ config.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ config.instrument_methods(const) if const.is_a?(Module)
+ end
+ end
+
+ GC::Profiler.enable
+
+ Gitlab::Metrics::Sampler.new.start
+end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 70ed10e8275..4c164119fff 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -16,7 +16,7 @@ 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)
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.new(env).call
+ OmniAuth::RequestForgeryProtection.call(env)
end
if Gitlab.config.omniauth.enabled
diff --git a/config/initializers/rack_attack.rb.example b/config/initializers/rack_attack.rb.example
index 2155ea14562..b1bbcca1d61 100644
--- a/config/initializers/rack_attack.rb.example
+++ b/config/initializers/rack_attack.rb.example
@@ -4,13 +4,13 @@
# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
paths_to_be_protected = [
- "#{Gitlab::Application.config.relative_url_root}/users/password",
- "#{Gitlab::Application.config.relative_url_root}/users/sign_in",
- "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session.json",
- "#{Gitlab::Application.config.relative_url_root}/api/#{API::API.version}/session",
- "#{Gitlab::Application.config.relative_url_root}/users",
- "#{Gitlab::Application.config.relative_url_root}/users/confirmation",
- "#{Gitlab::Application.config.relative_url_root}/unsubscribes/"
+ "#{Rails.application.config.relative_url_root}/users/password",
+ "#{Rails.application.config.relative_url_root}/users/sign_in",
+ "#{Rails.application.config.relative_url_root}/api/#{API::API.version}/session.json",
+ "#{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/"
]
diff --git a/config/initializers/rack_lineprof.rb b/config/initializers/rack_lineprof.rb
index f0c006d811b..22e77a32c61 100644
--- a/config/initializers/rack_lineprof.rb
+++ b/config/initializers/rack_lineprof.rb
@@ -2,7 +2,7 @@
# with darker backgrounds. This patch tweaks the colors a bit so the output is
# actually readable.
if Rails.env.development? and RUBY_ENGINE == 'ruby' and ENV['ENABLE_LINEPROF']
- Gitlab::Application.config.middleware.use(Rack::Lineprof)
+ Rails.application.config.middleware.use(Rack::Lineprof)
module Rack
class Lineprof
diff --git a/config/initializers/rack_profiler.rb b/config/initializers/rack_profiler.rb
deleted file mode 100644
index 7710eeac453..00000000000
--- a/config/initializers/rack_profiler.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-if Rails.env.development?
- require 'rack-mini-profiler'
-
- # initialization is skipped so trigger it
- Rack::MiniProfilerRails.initialize!(Gitlab::Application)
-
- Rack::MiniProfiler.config.position = 'right'
- Rack::MiniProfiler.config.start_hidden = false
- Rack::MiniProfiler.config.skip_paths << '/teaspoon'
-end
diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 1b518c3becf..dae3a4a9a93 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -22,15 +22,15 @@ def find_secure_token
end
end
-Gitlab::Application.config.secret_token = find_secure_token
-Gitlab::Application.config.secret_key_base = find_secure_token
+Rails.application.config.secret_token = find_secure_token
+Rails.application.config.secret_key_base = find_secure_token
# CI
def generate_new_secure_token
SecureRandom.hex(64)
end
-if Gitlab::Application.secrets.db_key_base.blank?
+if Rails.application.secrets.db_key_base.blank?
warn "Missing `db_key_base` for '#{Rails.env}' environment. The secrets will be generated and stored in `config/secrets.yml`"
all_secrets = YAML.load_file('config/secrets.yml') if File.exist?('config/secrets.yml')
@@ -46,5 +46,5 @@ if Gitlab::Application.secrets.db_key_base.blank?
file.write(YAML.dump(all_secrets))
end
- Gitlab::Application.secrets.db_key_base = env_secrets['db_key_base']
+ Rails.application.secrets.db_key_base = env_secrets['db_key_base']
end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 04ed9e90df5..0fc725842ba 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -3,18 +3,23 @@
require 'gitlab/current_settings'
include Gitlab::CurrentSettings
-# allow it to fail: it may to do so when create_from_defaults is executed before migrations are actually done
+# allow it to fail: it may do so when create_from_defaults is executed before migrations are actually done
begin
- Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay
+ Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay || 10080
rescue
+ Settings.gitlab['session_expire_delay'] ||= 10080
end
-Gitlab::Application.config.session_store(
- :redis_store, # Using the cookie_store would enable session replay attacks.
- servers: Gitlab::Application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
- key: '_gitlab_session',
- secure: Gitlab.config.gitlab.https,
- httponly: true,
- expire_after: Settings.gitlab['session_expire_delay'] * 60,
- path: (Gitlab::Application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root
-)
+if Rails.env.test?
+ Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
+else
+ Gitlab::Application.config.session_store(
+ :redis_store, # Using the cookie_store would enable session replay attacks.
+ servers: Rails.application.config.cache_store[1].merge(namespace: 'session:gitlab'), # re-use the Redis config from the Rails cache store
+ key: '_gitlab_session',
+ secure: Gitlab.config.gitlab.https,
+ httponly: true,
+ expire_after: Settings.gitlab['session_expire_delay'] * 60,
+ path: (Rails.application.config.relative_url_root.nil?) ? '/' : Gitlab::Application.config.relative_url_root
+ )
+end
diff --git a/config/initializers/sherlock.rb b/config/initializers/sherlock.rb
new file mode 100644
index 00000000000..8f2ababb712
--- /dev/null
+++ b/config/initializers/sherlock.rb
@@ -0,0 +1,5 @@
+if Gitlab::Sherlock.enabled?
+ Rails.application.configure do |config|
+ config.middleware.use(Gitlab::Sherlock::Middleware)
+ end
+end
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index e856499732e..dcf6ce74d96 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -17,6 +17,21 @@ Sidekiq.configure_server do |config|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
end
+
+ # Sidekiq-cron: load recurring jobs from gitlab.yml
+ # 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') }
+ Sidekiq::Cron::Job.load_from_hash! cron_jobs
+
+ # Database pool should be at least `sidekiq_concurrency` + 2
+ # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
+ config = ActiveRecord::Base.configurations[Rails.env] ||
+ Rails.application.config.database_configuration[Rails.env]
+ 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')}")
end
Sidekiq.configure_client do |config|
diff --git a/config/initializers/smtp_settings.rb.sample b/config/initializers/smtp_settings.rb.sample
index 25ec247a095..ec182502d4e 100644
--- a/config/initializers/smtp_settings.rb.sample
+++ b/config/initializers/smtp_settings.rb.sample
@@ -8,7 +8,7 @@
# If you change this file in a Merge Request, please also create a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
if Rails.env.production?
- Gitlab::Application.config.action_mailer.delivery_method = :smtp
+ Rails.application.config.action_mailer.delivery_method = :smtp
ActionMailer::Base.smtp_settings = {
address: "email.server.com",
diff --git a/config/initializers/state_machine_patch.rb b/config/initializers/state_machine_patch.rb
deleted file mode 100644
index 72d010fa5de..00000000000
--- a/config/initializers/state_machine_patch.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# This is a patch to address the issue in https://github.com/pluginaweek/state_machine/issues/251
-# where gem 'state_machine' was not working for Rails 4.1
-module StateMachine
- module Integrations
- module ActiveModel
- public :around_validation
- end
- end
-end
diff --git a/config/initializers/static_files.rb b/config/initializers/static_files.rb
index e6d5600edb7..d6dbf8b9fbf 100644
--- a/config/initializers/static_files.rb
+++ b/config/initializers/static_files.rb
@@ -1,6 +1,6 @@
-app = Gitlab::Application
+app = Rails.application
-if app.config.serve_static_assets
+if app.config.serve_static_files
# The `ActionDispatch::Static` middleware intercepts requests for static files
# by checking if they exist in the `/public` directory.
# We're replacing it with our `Gitlab::Middleware::Static` that does the same,
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index 22070e37f07..bd4c3ebc69e 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -30,7 +30,6 @@ en:
success: "Successfully authenticated from %{kind} account."
passwords:
no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided."
- recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again."
send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes."
updated: "Your password has been changed successfully. You are now signed in."
diff --git a/config/locales/sherlock.en.yml b/config/locales/sherlock.en.yml
new file mode 100644
index 00000000000..f24b825f585
--- /dev/null
+++ b/config/locales/sherlock.en.yml
@@ -0,0 +1,38 @@
+en:
+ sherlock:
+ title: Sherlock
+ delete_all_transactions: Delete All Transactions
+ introduction: >
+ Below is a list of all transactions recorded by Sherlock. Requests to
+ Sherlock's own routes are ignored.
+ no_transactions: No transactions to show
+ no_queries: No queries to show
+ no_file_samples: No file samples to show
+ all_transactions: All Transactions
+ transaction: Transaction
+ query: Query
+ file_sample: File Sample
+ type: Type
+ path: Path
+ time: Time
+ queries: Queries
+ finished_at: Finished at
+ ago: ago
+ view: View
+ seconds: seconds
+ milliseconds: ms
+ general: General
+ id: ID
+ time_inclusive: Time (inclusive)
+ backtrace: Backtrace
+ application_backtrace: Application Backtrace
+ full_backtrace: Full Backtrace
+ origin: Origin
+ line: line
+ line_capitalized: Line
+ copy_to_clipboard: Copy to clipboard
+ query_plan: Query Plan
+ events: Events
+ percent: '%'
+ count: Count
+ query_time: Query Time
diff --git a/config/routes.rb b/config/routes.rb
index f6e17a21479..1aa6eed8c94 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -1,7 +1,21 @@
require 'sidekiq/web'
+require 'sidekiq/cron/web'
require 'api/api'
-Gitlab::Application.routes.draw do
+Rails.application.routes.draw do
+ if Gitlab::Sherlock.enabled?
+ namespace :sherlock do
+ resources :transactions, only: [:index, :show] do
+ resources :queries, only: [:show]
+ resources :file_samples, only: [:show]
+
+ collection do
+ delete :destroy_all
+ end
+ end
+ end
+ end
+
namespace :ci do
# CI API
Ci::API::API.logger Rails.logger
@@ -10,46 +24,10 @@ Gitlab::Application.routes.draw do
resource :lint, only: [:show, :create]
resources :projects do
- collection do
- post :add
- get :disabled
- end
-
member do
get :status, to: 'projects#badge'
get :integration
- post :toggle_shared_runners
- get :dumped_yaml
- end
-
- resources :runner_projects, only: [:create, :destroy]
-
- resources :events, only: [:index]
- end
-
- resource :user_sessions do
- get :auth
- get :callback
- end
-
- namespace :admin do
- resources :runners, only: [:index, :show, :update, :destroy] do
- member do
- put :assign_all
- get :resume
- get :pause
- end
end
-
- resources :events, only: [:index]
-
- resources :projects do
- resources :runner_projects
- end
-
- resources :builds, only: :index
-
- resource :application_settings, only: [:show, :update]
end
root to: 'projects#index'
@@ -83,7 +61,7 @@ Gitlab::Application.routes.draw do
end
# Enable Grack support
- mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]
+ mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put]
# Help
get 'help' => 'help#index'
@@ -210,7 +188,9 @@ Gitlab::Application.routes.draw do
namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
- resources :identities, only: [:index, :edit, :update, :destroy]
+ resources :identities, except: [:show]
+
+ delete 'stop_impersonation' => 'impersonation#destroy', on: :collection
member do
get :projects
@@ -221,7 +201,7 @@ Gitlab::Application.routes.draw do
put :unblock
put :unlock
put :confirm
- post :login_as
+ post 'impersonate' => 'impersonation#create'
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
end
@@ -258,15 +238,31 @@ Gitlab::Application.routes.draw do
member do
put :transfer
end
+
+ resources :runner_projects
end
end
resource :application_settings, only: [:show, :update] do
resources :services
+ put :reset_runners_token
end
resources :labels
+ resources :runners, only: [:index, :show, :update, :destroy] do
+ member do
+ get :resume
+ get :pause
+ end
+ end
+
+ resources :builds, only: :index do
+ collection do
+ post :cancel_all
+ end
+ end
+
root to: 'dashboard#index'
end
@@ -301,6 +297,7 @@ Gitlab::Application.routes.draw do
resource :two_factor_auth, only: [:new, :create, :destroy] do
member do
post :codes
+ patch :skip
end
end
end
@@ -356,7 +353,7 @@ Gitlab::Application.routes.draw do
end
resource :avatar, only: [:destroy]
- resources :milestones, only: [:index, :show, :update]
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
end
end
@@ -446,7 +443,7 @@ Gitlab::Application.routes.draw do
scope do
post(
- '/create_dir/*id',
+ '/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
@@ -475,8 +472,9 @@ Gitlab::Application.routes.draw do
resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do
member do
get :branches
- get :ci
- get :cancel_builds
+ get :builds
+ post :cancel_builds
+ post :retry_builds
end
end
@@ -487,6 +485,7 @@ Gitlab::Application.routes.draw do
member do
get :commits
get :ci
+ get :languages
end
end
@@ -556,10 +555,12 @@ Gitlab::Application.routes.draw do
resources :merge_requests, constraints: { id: /\d+/ }, except: [:destroy] do
member do
- get :diffs
get :commits
- post :merge
+ get :diffs
+ get :builds
get :merge_check
+ post :merge
+ post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
end
@@ -572,31 +573,23 @@ Gitlab::Application.routes.draw do
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } 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 :triggers, only: [:index, :create, :destroy]
- resource :ci_settings, only: [:edit, :update, :destroy]
- resources :ci_web_hooks, only: [:index, :create, :destroy] do
- member do
- get :test
- end
- end
-
- resources :ci_services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
- member do
- get :test
- end
- end
resources :builds, only: [:index, :show] do
collection do
- get :cancel_all
+ post :cancel_all
end
member do
- get :cancel
get :status
+ post :cancel
+ get :download
post :retry
end
end
@@ -648,6 +641,10 @@ Gitlab::Application.routes.draw do
member do
delete :delete_attachment
end
+
+ collection do
+ post :award_toggle
+ end
end
resources :uploads, only: [:create] do
@@ -661,7 +658,13 @@ Gitlab::Application.routes.draw do
get :resume
get :pause
end
+
+ collection do
+ post :toggle_shared_runners
+ end
end
+
+ resources :runner_projects, only: [:create, :destroy]
end
end
end
diff --git a/config/schedule.rb b/config/schedule.rb
deleted file mode 100644
index 8122f7cc69c..00000000000
--- a/config/schedule.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# Use this file to easily define all of your cron jobs.
-#
-# If you make changes to this file, please create also an issue on
-# https://gitlab.com/gitlab-org/omnibus-gitlab/issues . This is necessary
-# because the omnibus packages manage cron jobs using Chef instead of Whenever.
-every 1.hour do
- rake "ci:schedule_builds"
-end
diff --git a/db/migrate/20121220064453_init_schema.rb b/db/migrate/20121220064453_init_schema.rb
index 90f5eb08e8c..d7644b6847a 100644
--- a/db/migrate/20121220064453_init_schema.rb
+++ b/db/migrate/20121220064453_init_schema.rb
@@ -1,6 +1,6 @@
class InitSchema < ActiveRecord::Migration
def up
-
+
create_table "events", force: true do |t|
t.string "target_type"
t.integer "target_id"
@@ -12,14 +12,14 @@ class InitSchema < ActiveRecord::Migration
t.integer "action"
t.integer "author_id"
end
-
+
add_index "events", ["action"], name: "index_events_on_action", using: :btree
add_index "events", ["author_id"], name: "index_events_on_author_id", using: :btree
add_index "events", ["created_at"], name: "index_events_on_created_at", using: :btree
add_index "events", ["project_id"], name: "index_events_on_project_id", using: :btree
add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
-
+
create_table "issues", force: true do |t|
t.string "title"
t.integer "assignee_id"
@@ -33,7 +33,7 @@ class InitSchema < ActiveRecord::Migration
t.text "description"
t.integer "milestone_id"
end
-
+
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
add_index "issues", ["closed"], name: "index_issues_on_closed", using: :btree
@@ -41,7 +41,7 @@ class InitSchema < ActiveRecord::Migration
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
-
+
create_table "keys", force: true do |t|
t.integer "user_id"
t.datetime "created_at"
@@ -51,11 +51,11 @@ class InitSchema < ActiveRecord::Migration
t.string "identifier"
t.integer "project_id"
end
-
+
add_index "keys", ["identifier"], name: "index_keys_on_identifier", using: :btree
add_index "keys", ["project_id"], name: "index_keys_on_project_id", using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
-
+
create_table "merge_requests", force: true do |t|
t.string "target_branch", null: false
t.string "source_branch", null: false
@@ -66,13 +66,13 @@ class InitSchema < ActiveRecord::Migration
t.boolean "closed", default: false, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.text "st_commits", limit: 2147483647
- t.text "st_diffs", limit: 2147483647
+ t.text "st_commits"
+ t.text "st_diffs"
t.boolean "merged", default: false, null: false
t.integer "state", default: 1, null: false
t.integer "milestone_id"
end
-
+
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
add_index "merge_requests", ["author_id"], name: "index_merge_requests_on_author_id", using: :btree
add_index "merge_requests", ["closed"], name: "index_merge_requests_on_closed", using: :btree
@@ -82,7 +82,7 @@ class InitSchema < ActiveRecord::Migration
add_index "merge_requests", ["source_branch"], name: "index_merge_requests_on_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
-
+
create_table "milestones", force: true do |t|
t.string "title", null: false
t.integer "project_id", null: false
@@ -92,10 +92,10 @@ class InitSchema < ActiveRecord::Migration
t.datetime "created_at"
t.datetime "updated_at"
end
-
+
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
-
+
create_table "namespaces", force: true do |t|
t.string "name", null: false
t.string "path", null: false
@@ -104,12 +104,12 @@ class InitSchema < ActiveRecord::Migration
t.datetime "updated_at"
t.string "type"
end
-
+
add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
-
+
create_table "notes", force: true do |t|
t.text "note"
t.string "noteable_type"
@@ -122,13 +122,13 @@ class InitSchema < ActiveRecord::Migration
t.string "commit_id"
t.integer "noteable_id"
end
-
+
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
add_index "notes", ["project_id", "noteable_type"], name: "index_notes_on_project_id_and_noteable_type", using: :btree
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
-
+
create_table "projects", force: true do |t|
t.string "name"
t.string "path"
@@ -144,17 +144,17 @@ class InitSchema < ActiveRecord::Migration
t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
end
-
+
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
add_index "projects", ["owner_id"], name: "index_projects_on_owner_id", using: :btree
-
+
create_table "protected_branches", force: true do |t|
t.integer "project_id", null: false
t.string "name", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
-
+
create_table "services", force: true do |t|
t.string "type"
t.string "title"
@@ -165,9 +165,9 @@ class InitSchema < ActiveRecord::Migration
t.boolean "active", default: false, null: false
t.string "project_url"
end
-
+
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
-
+
create_table "snippets", force: true do |t|
t.string "title"
t.text "content"
@@ -178,11 +178,11 @@ class InitSchema < ActiveRecord::Migration
t.string "file_name"
t.datetime "expires_at"
end
-
+
add_index "snippets", ["created_at"], name: "index_snippets_on_created_at", using: :btree
add_index "snippets", ["expires_at"], name: "index_snippets_on_expires_at", using: :btree
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
-
+
create_table "taggings", force: true do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -192,14 +192,14 @@ class InitSchema < ActiveRecord::Migration
t.string "context"
t.datetime "created_at"
end
-
+
add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
+
create_table "tags", force: true do |t|
t.string "name"
end
-
+
create_table "user_team_project_relationships", force: true do |t|
t.integer "project_id"
t.integer "user_team_id"
@@ -207,7 +207,7 @@ class InitSchema < ActiveRecord::Migration
t.datetime "created_at"
t.datetime "updated_at"
end
-
+
create_table "user_team_user_relationships", force: true do |t|
t.integer "user_id"
t.integer "user_team_id"
@@ -216,7 +216,7 @@ class InitSchema < ActiveRecord::Migration
t.datetime "created_at"
t.datetime "updated_at"
end
-
+
create_table "user_teams", force: true do |t|
t.string "name"
t.string "path"
@@ -224,7 +224,7 @@ class InitSchema < ActiveRecord::Migration
t.datetime "created_at"
t.datetime "updated_at"
end
-
+
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -255,7 +255,7 @@ class InitSchema < ActiveRecord::Migration
t.string "provider"
t.string "username"
end
-
+
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
add_index "users", ["blocked"], name: "index_users_on_blocked", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
@@ -263,7 +263,7 @@ class InitSchema < ActiveRecord::Migration
add_index "users", ["name"], name: "index_users_on_name", using: :btree
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
-
+
create_table "users_projects", force: true do |t|
t.integer "user_id", null: false
t.integer "project_id", null: false
@@ -271,11 +271,11 @@ class InitSchema < ActiveRecord::Migration
t.datetime "updated_at"
t.integer "project_access", default: 0, null: false
end
-
+
add_index "users_projects", ["project_access"], name: "index_users_projects_on_project_access", using: :btree
add_index "users_projects", ["project_id"], name: "index_users_projects_on_project_id", using: :btree
add_index "users_projects", ["user_id"], name: "index_users_projects_on_user_id", using: :btree
-
+
create_table "web_hooks", force: true do |t|
t.string "url"
t.integer "project_id"
@@ -284,7 +284,7 @@ class InitSchema < ActiveRecord::Migration
t.string "type", default: "ProjectHook"
t.integer "service_id"
end
-
+
create_table "wikis", force: true do |t|
t.string "title"
t.text "content"
@@ -294,10 +294,10 @@ class InitSchema < ActiveRecord::Migration
t.string "slug"
t.integer "user_id"
end
-
+
add_index "wikis", ["project_id"], name: "index_wikis_on_project_id", using: :btree
add_index "wikis", ["slug"], name: "index_wikis_on_slug", using: :btree
-
+
end
def down
diff --git a/db/migrate/20140122112253_create_merge_request_diffs.rb b/db/migrate/20140122112253_create_merge_request_diffs.rb
index ef592305a23..f34e30925df 100644
--- a/db/migrate/20140122112253_create_merge_request_diffs.rb
+++ b/db/migrate/20140122112253_create_merge_request_diffs.rb
@@ -1,12 +1,21 @@
class CreateMergeRequestDiffs < ActiveRecord::Migration
- def change
+ def up
create_table :merge_request_diffs do |t|
t.string :state, null: false, default: 'collected'
- t.text :st_commits, null: true, limit: 2147483647
- t.text :st_diffs, null: true, limit: 2147483647
+ t.text :st_commits, null: true
+ t.text :st_diffs, null: true
t.integer :merge_request_id, null: false
t.timestamps
end
+
+ if ActiveRecord::Base.configurations[Rails.env]['adapter'] =~ /^mysql/
+ change_column :merge_request_diffs, :st_commits, :text, limit: 2147483647
+ change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
+ end
+ end
+
+ def down
+ drop_table :merge_request_diffs
end
end
diff --git a/db/migrate/20140903115954_migrate_to_new_shell.rb b/db/migrate/20140903115954_migrate_to_new_shell.rb
index 2d832109513..54cbe48960a 100644
--- a/db/migrate/20140903115954_migrate_to_new_shell.rb
+++ b/db/migrate/20140903115954_migrate_to_new_shell.rb
@@ -1,5 +1,7 @@
class MigrateToNewShell < ActiveRecord::Migration
def change
+ return if Rails.env.test?
+
gitlab_shell_path = Gitlab.config.gitlab_shell.path
if system("#{gitlab_shell_path}/bin/create-hooks")
puts 'Repositories updated with new hooks'
diff --git a/db/migrate/20151012173029_set_jira_service_api_url.rb b/db/migrate/20151012173029_set_jira_service_api_url.rb
new file mode 100644
index 00000000000..2af99e0db0b
--- /dev/null
+++ b/db/migrate/20151012173029_set_jira_service_api_url.rb
@@ -0,0 +1,50 @@
+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.
+
+ def build_api_url_from_project_url(project_url, api_version)
+ # this is the exact logic previously used to build the Jira API URL from project_url
+ server = URI(project_url)
+ default_ports = [80, 443].include?(server.port)
+ server_url = "#{server.scheme}://#{server.host}"
+ server_url.concat(":#{server.port}") unless default_ports
+ "#{server_url}/rest/api/#{api_version}"
+ end
+
+ def get_api_version_from_api_url(api_url)
+ match = /\/rest\/api\/(?<api_version>\w+)$/.match(api_url)
+ match && match['api_version']
+ end
+
+ def change
+ reversible do |dir|
+ select_all("SELECT id, properties FROM services WHERE services.type IN ('JiraService')").each do |jira_service|
+ id = jira_service["id"]
+ properties = JSON.parse(jira_service["properties"])
+ properties_was = properties.clone
+
+ dir.up do
+ # remove api_version and set api_url
+ if properties['api_version'].present? && properties['project_url'].present?
+ begin
+ properties['api_url'] ||= build_api_url_from_project_url(properties['project_url'], properties['api_version'])
+ rescue
+ # looks like project_url was not a valid URL. Do nothing.
+ end
+ end
+ properties.delete('api_version') if properties.include?('api_version')
+ end
+
+ dir.down do
+ # remove api_url and set api_version (default to '2')
+ properties['api_version'] ||= get_api_version_from_api_url(properties['api_url']) || '2'
+ properties.delete('api_url') if properties.include?('api_url')
+ end
+
+ if properties != properties_was
+ execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
+ end
+ end
+ end
+ end
+end
diff --git a/db/migrate/20151013092124_add_artifacts_file_to_builds.rb b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
new file mode 100644
index 00000000000..5a299f7b26d
--- /dev/null
+++ b/db/migrate/20151013092124_add_artifacts_file_to_builds.rb
@@ -0,0 +1,5 @@
+class AddArtifactsFileToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_file, :text
+ end
+end
diff --git a/db/migrate/20151019111551_fix_build_tags.rb b/db/migrate/20151019111551_fix_build_tags.rb
index 84b142183f8..299a24b0a7c 100644
--- a/db/migrate/20151019111551_fix_build_tags.rb
+++ b/db/migrate/20151019111551_fix_build_tags.rb
@@ -1,5 +1,9 @@
class FixBuildTags < ActiveRecord::Migration
- def change
+ def up
execute("UPDATE taggings SET taggable_type='CommitStatus' WHERE taggable_type='Ci::Build'")
end
+
+ def down
+ execute("UPDATE taggings SET taggable_type='Ci::Build' WHERE taggable_type='CommitStatus'")
+ end
end
diff --git a/db/migrate/20151019111703_fail_build_without_names.rb b/db/migrate/20151019111703_fail_build_without_names.rb
index 546b03d8129..dcdb5d1b25d 100644
--- a/db/migrate/20151019111703_fail_build_without_names.rb
+++ b/db/migrate/20151019111703_fail_build_without_names.rb
@@ -1,5 +1,8 @@
class FailBuildWithoutNames < ActiveRecord::Migration
- def change
+ def up
execute("UPDATE ci_builds SET status='failed' WHERE name IS NULL AND status='pending'")
end
+
+ def down
+ end
end
diff --git a/db/migrate/20151020145526_add_services_template_index.rb b/db/migrate/20151020145526_add_services_template_index.rb
new file mode 100644
index 00000000000..1b04f313565
--- /dev/null
+++ b/db/migrate/20151020145526_add_services_template_index.rb
@@ -0,0 +1,5 @@
+class AddServicesTemplateIndex < ActiveRecord::Migration
+ def change
+ add_index :services, :template
+ end
+end
diff --git a/db/migrate/20151023112551_fail_build_with_empty_name.rb b/db/migrate/20151023112551_fail_build_with_empty_name.rb
new file mode 100644
index 00000000000..41c0f0649cd
--- /dev/null
+++ b/db/migrate/20151023112551_fail_build_with_empty_name.rb
@@ -0,0 +1,8 @@
+class FailBuildWithEmptyName < ActiveRecord::Migration
+ def up
+ execute("UPDATE ci_builds SET status='failed' WHERE (name IS NULL OR name='') AND status='pending'")
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20151023144219_remove_satellites.rb b/db/migrate/20151023144219_remove_satellites.rb
new file mode 100644
index 00000000000..e73f300028a
--- /dev/null
+++ b/db/migrate/20151023144219_remove_satellites.rb
@@ -0,0 +1,17 @@
+require 'fileutils'
+
+class RemoveSatellites < ActiveRecord::Migration
+ def up
+ satellites = Gitlab.config['satellites']
+ return if satellites.nil?
+
+ satellites_path = satellites['path']
+ return if satellites_path.nil?
+
+ FileUtils.rm_rf(satellites_path)
+ end
+
+ def down
+ # Do nothing
+ end
+end
diff --git a/db/migrate/20151026182941_add_project_path_index.rb b/db/migrate/20151026182941_add_project_path_index.rb
new file mode 100644
index 00000000000..a62fe199d70
--- /dev/null
+++ b/db/migrate/20151026182941_add_project_path_index.rb
@@ -0,0 +1,9 @@
+class AddProjectPathIndex < ActiveRecord::Migration
+ def up
+ add_index :projects, :path
+ end
+
+ def down
+ remove_index :projects, :path
+ end
+end
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
new file mode 100644
index 00000000000..ceb52f0c222
--- /dev/null
+++ b/db/migrate/20151028152939_add_merge_when_build_succeeds_to_merge_request.rb
@@ -0,0 +1,7 @@
+class AddMergeWhenBuildSucceedsToMergeRequest < ActiveRecord::Migration
+ def change
+ add_column :merge_requests, :merge_params, :text
+ add_column :merge_requests, :merge_when_build_succeeds, :boolean, default: false, null: false
+ add_column :merge_requests, :merge_user_id, :integer
+ end
+end
diff --git a/db/migrate/20151103001141_add_public_to_group.rb b/db/migrate/20151103001141_add_public_to_group.rb
new file mode 100644
index 00000000000..635346300c2
--- /dev/null
+++ b/db/migrate/20151103001141_add_public_to_group.rb
@@ -0,0 +1,5 @@
+class AddPublicToGroup < ActiveRecord::Migration
+ def change
+ add_column :namespaces, :public, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20151103133339_add_shared_runners_setting.rb b/db/migrate/20151103133339_add_shared_runners_setting.rb
new file mode 100644
index 00000000000..4231dfd5c2e
--- /dev/null
+++ b/db/migrate/20151103133339_add_shared_runners_setting.rb
@@ -0,0 +1,5 @@
+class AddSharedRunnersSetting < ActiveRecord::Migration
+ def up
+ add_column :application_settings, :shared_runners_enabled, :boolean, default: true, null: false
+ end
+end
diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb
new file mode 100644
index 00000000000..2d04c170a88
--- /dev/null
+++ b/db/migrate/20151103134857_create_lfs_objects.rb
@@ -0,0 +1,10 @@
+class CreateLfsObjects < ActiveRecord::Migration
+ def change
+ create_table :lfs_objects do |t|
+ t.string :oid, null: false, unique: true
+ t.integer :size, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb
new file mode 100644
index 00000000000..f3f58b931ec
--- /dev/null
+++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb
@@ -0,0 +1,12 @@
+class CreateLfsObjectsProjects < ActiveRecord::Migration
+ def change
+ create_table :lfs_objects_projects do |t|
+ t.integer :lfs_object_id, null: false
+ t.integer :project_id, null: false
+
+ t.timestamps
+ end
+
+ add_index :lfs_objects_projects, :project_id
+ end
+end
diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
new file mode 100644
index 00000000000..7c57f3f0df6
--- /dev/null
+++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
@@ -0,0 +1,5 @@
+class AddFileToLfsObjects < ActiveRecord::Migration
+ def change
+ add_column :lfs_objects, :file, :string
+ end
+end
diff --git a/db/migrate/20151105094515_create_releases.rb b/db/migrate/20151105094515_create_releases.rb
new file mode 100644
index 00000000000..fe4608c6662
--- /dev/null
+++ b/db/migrate/20151105094515_create_releases.rb
@@ -0,0 +1,14 @@
+class CreateReleases < ActiveRecord::Migration
+ def change
+ create_table :releases do |t|
+ t.string :tag
+ t.text :description
+ t.integer :project_id
+
+ t.timestamps
+ end
+
+ add_index :releases, :project_id
+ add_index :releases, [:project_id, :tag]
+ end
+end
diff --git a/db/migrate/20151106000015_add_is_award_to_notes.rb b/db/migrate/20151106000015_add_is_award_to_notes.rb
new file mode 100644
index 00000000000..02b271637e9
--- /dev/null
+++ b/db/migrate/20151106000015_add_is_award_to_notes.rb
@@ -0,0 +1,6 @@
+class AddIsAwardToNotes < ActiveRecord::Migration
+ def change
+ add_column :notes, :is_award, :boolean, default: false, null: false
+ add_index :notes, :is_award
+ end
+end
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
new file mode 100644
index 00000000000..01d8c0f043e
--- /dev/null
+++ b/db/migrate/20151109100728_add_max_artifacts_size_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddMaxArtifactsSizeToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :max_artifacts_size, :integer, default: 100, null: false
+ end
+end
diff --git a/db/migrate/20151109134526_add_issues_state_index.rb b/db/migrate/20151109134526_add_issues_state_index.rb
new file mode 100644
index 00000000000..1c4d2e30171
--- /dev/null
+++ b/db/migrate/20151109134526_add_issues_state_index.rb
@@ -0,0 +1,5 @@
+class AddIssuesStateIndex < ActiveRecord::Migration
+ def change
+ add_index :issues, :state
+ end
+end
diff --git a/db/migrate/20151109134916_add_projects_visibility_level_index.rb b/db/migrate/20151109134916_add_projects_visibility_level_index.rb
new file mode 100644
index 00000000000..600b4bafd98
--- /dev/null
+++ b/db/migrate/20151109134916_add_projects_visibility_level_index.rb
@@ -0,0 +1,5 @@
+class AddProjectsVisibilityLevelIndex < ActiveRecord::Migration
+ def change
+ add_index :projects, :visibility_level
+ end
+end
diff --git a/db/migrate/20151110125604_add_import_error_to_project.rb b/db/migrate/20151110125604_add_import_error_to_project.rb
new file mode 100644
index 00000000000..7fc990f8d0a
--- /dev/null
+++ b/db/migrate/20151110125604_add_import_error_to_project.rb
@@ -0,0 +1,5 @@
+class AddImportErrorToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :import_error, :text
+ end
+end
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
new file mode 100644
index 00000000000..d10f1f6e605
--- /dev/null
+++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
@@ -0,0 +1,6 @@
+class AddIndexForLfsOidAndSize < ActiveRecord::Migration
+ def change
+ add_index :lfs_objects, :oid
+ add_index :lfs_objects, [:oid, :size]
+ end
+end
diff --git a/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
new file mode 100644
index 00000000000..41b93da0a86
--- /dev/null
+++ b/db/migrate/20151116144118_add_unique_for_lfs_oid_index.rb
@@ -0,0 +1,7 @@
+class AddUniqueForLfsOidIndex < ActiveRecord::Migration
+ def change
+ remove_index :lfs_objects, :oid
+ remove_index :lfs_objects, [:oid, :size]
+ add_index :lfs_objects, :oid, unique: true
+ end
+end
diff --git a/db/migrate/20151118162244_add_projects_public_index.rb b/db/migrate/20151118162244_add_projects_public_index.rb
new file mode 100644
index 00000000000..fded70e3c0c
--- /dev/null
+++ b/db/migrate/20151118162244_add_projects_public_index.rb
@@ -0,0 +1,5 @@
+class AddProjectsPublicIndex < ActiveRecord::Migration
+ def change
+ add_index :namespaces, :public
+ end
+end
diff --git a/db/migrate/20151203162133_add_hide_project_limit_to_users.rb b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
new file mode 100644
index 00000000000..6ffadfa1894
--- /dev/null
+++ b/db/migrate/20151203162133_add_hide_project_limit_to_users.rb
@@ -0,0 +1,5 @@
+class AddHideProjectLimitToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :hide_project_limit, :boolean, default: false
+ end
+end
diff --git a/db/migrate/20151203162134_add_build_events_to_services.rb b/db/migrate/20151203162134_add_build_events_to_services.rb
new file mode 100644
index 00000000000..c5542cb864d
--- /dev/null
+++ b/db/migrate/20151203162134_add_build_events_to_services.rb
@@ -0,0 +1,6 @@
+class AddBuildEventsToServices < ActiveRecord::Migration
+ def change
+ add_column :services, :build_events, :boolean, default: false, null: false
+ add_column :web_hooks, :build_events, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
new file mode 100644
index 00000000000..d7e196e6763
--- /dev/null
+++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
@@ -0,0 +1,16 @@
+class MigrateCiWebHooks < ActiveRecord::Migration
+ include Gitlab::Database
+
+ def up
+ execute(
+ 'INSERT INTO web_hooks (url, project_id, type, created_at, updated_at, push_events, issues_events, merge_requests_events, tag_push_events, note_events, build_events) ' \
+ "SELECT ci_web_hooks.url, projects.id, 'ProjectHook', ci_web_hooks.created_at, ci_web_hooks.updated_at, " \
+ "#{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value} FROM ci_web_hooks " \
+ 'JOIN ci_projects ON ci_web_hooks.project_id = ci_projects.id ' \
+ 'JOIN projects ON ci_projects.gitlab_id = projects.id'
+ )
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb
new file mode 100644
index 00000000000..7f330a2cf0a
--- /dev/null
+++ b/db/migrate/20151209145909_migrate_ci_emails.rb
@@ -0,0 +1,45 @@
+class MigrateCiEmails < ActiveRecord::Migration
+ include Gitlab::Database
+
+ def up
+ # This inserts a new service: BuildsEmailService
+ # It "manually" constructs the properties (JSON-encoded)
+ # Migrating all ci_projects e-mail related columns
+ execute(
+ 'INSERT INTO services (project_id, type, created_at, updated_at, active, push_events, issues_events, merge_requests_events, tag_push_events, note_events, build_events, properties) ' \
+ "SELECT projects.id, 'BuildsEmailService', ci_services.created_at, ci_services.updated_at, " \
+ "#{true_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{false_value}, #{true_value}, " \
+ "CONCAT('{\"notify_only_broken_builds\":\"', #{convert_bool('ci_projects.email_only_broken_builds')}, " \
+ "'\",\"add_pusher\":\"', #{convert_bool('ci_projects.email_add_pusher')}, " \
+ "'\",\"recipients\":\"', #{escape_text('ci_projects.email_recipients')}, " \
+ "'\"}') " \
+ 'FROM ci_services ' \
+ 'JOIN ci_projects ON ci_services.project_id = ci_projects.id ' \
+ 'JOIN projects ON ci_projects.gitlab_id = projects.id ' \
+ "WHERE ci_services.type = 'Ci::MailService' AND ci_services.active"
+ )
+ end
+
+ def down
+ end
+
+ # This function escapes double-quotes and slash
+ def escape_text(name)
+ if Gitlab::Database.postgresql?
+ "REPLACE(REPLACE(#{name}, '\\', '\\\\'), '\"', '\\\"')"
+ else
+ "REPLACE(REPLACE(#{name}, '\\\\', '\\\\\\\\'), '\\\"', '\\\\\\\"')"
+ end
+ end
+
+ # This function returns 0 or 1 for column
+ def convert_bool(name)
+ if Gitlab::Database.postgresql?
+ # PostgreSQL uses BOOLEAN type
+ "CASE WHEN #{name} IS TRUE THEN '1' ELSE '0' END"
+ else
+ # MySQL uses TINYINT
+ "#{name}"
+ end
+ end
+end
diff --git a/db/migrate/20151210030143_add_unlock_token_to_user.rb b/db/migrate/20151210030143_add_unlock_token_to_user.rb
new file mode 100644
index 00000000000..0ea66ba65df
--- /dev/null
+++ b/db/migrate/20151210030143_add_unlock_token_to_user.rb
@@ -0,0 +1,5 @@
+class AddUnlockTokenToUser < ActiveRecord::Migration
+ def change
+ add_column :users, :unlock_token, :string
+ end
+end
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
new file mode 100644
index 00000000000..00f88180e46
--- /dev/null
+++ b/db/migrate/20151210072243_add_runners_registration_token_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddRunnersRegistrationTokenToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :runners_registration_token, :string
+ end
+end
diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb
new file mode 100644
index 00000000000..f14efa3e95d
--- /dev/null
+++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb
@@ -0,0 +1,33 @@
+class MigrateCiSlackService < ActiveRecord::Migration
+ include Gitlab::Database
+
+ def up
+ properties_query = 'SELECT properties FROM ci_services ' \
+ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \
+ "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::SlackService' AND ci_services.active " \
+ 'LIMIT 1'
+
+ active_query = 'SELECT 1 FROM ci_services ' \
+ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \
+ "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::SlackService' AND ci_services.active " \
+ 'LIMIT 1'
+
+ # We update the service since services are always generated for project, even if they are inactive
+ # Activate service and migrate properties if currently the service is not active
+ execute(
+ "UPDATE services SET properties=(#{properties_query}), active=#{true_value}, " \
+ "push_events=#{false_value}, issues_events=#{false_value}, merge_requests_events=#{false_value}, " \
+ "tag_push_events=#{false_value}, note_events=#{false_value}, build_events=#{true_value} " \
+ "WHERE NOT services.active AND services.type='SlackService' AND (#{active_query}) IS NOT NULL"
+ )
+
+ # Tick only build_events if the service is already active
+ execute(
+ "UPDATE services SET build_events=#{true_value} " \
+ "WHERE services.active AND services.type='SlackService' AND (#{active_query}) IS NOT NULL"
+ )
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
new file mode 100644
index 00000000000..b9e04323576
--- /dev/null
+++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
@@ -0,0 +1,34 @@
+class MigrateCiHipChatService < ActiveRecord::Migration
+ include Gitlab::Database
+
+ def up
+ # From properties strip `hipchat_` key
+ properties_query = "SELECT REPLACE(properties, '\"hipchat_', '\"') FROM ci_services " \
+ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \
+ "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::HipChatService' AND ci_services.active " \
+ 'LIMIT 1'
+
+ active_query = 'SELECT 1 FROM ci_services ' \
+ 'JOIN ci_projects ON ci_services.project_id=ci_projects.id ' \
+ "WHERE ci_projects.gitlab_id=services.project_id AND ci_services.type='Ci::HipChatService' AND ci_services.active " \
+ 'LIMIT 1'
+
+ # We update the service since services are always generated for project, even if they are inactive
+ # Activate service and migrate properties if currently the service is not active
+ execute(
+ "UPDATE services SET properties=(#{properties_query}), active=#{true_value}, " \
+ "push_events=#{false_value}, issues_events=#{false_value}, merge_requests_events=#{false_value}, " \
+ "tag_push_events=#{false_value}, note_events=#{false_value}, build_events=#{true_value} " \
+ "WHERE NOT services.active AND services.type='HipchatService' AND (#{active_query}) IS NOT NULL"
+ )
+
+ # Tick only build_events if the service is already active
+ execute(
+ "UPDATE services SET build_events=#{true_value} " \
+ "WHERE services.active AND services.type='HipchatService' AND (#{active_query}) IS NOT NULL"
+ )
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20151210125928_add_ci_to_project.rb b/db/migrate/20151210125928_add_ci_to_project.rb
new file mode 100644
index 00000000000..8c167f64a2b
--- /dev/null
+++ b/db/migrate/20151210125928_add_ci_to_project.rb
@@ -0,0 +1,11 @@
+class AddCiToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :ci_id, :integer
+ add_column :projects, :builds_enabled, :boolean, default: true, null: false
+ add_column :projects, :shared_runners_enabled, :boolean, default: true, null: false
+ add_column :projects, :runners_token, :string
+ add_column :projects, :build_coverage_regex, :string
+ add_column :projects, :build_allow_git_fetch, :boolean, default: true, null: false
+ add_column :projects, :build_timeout, :integer, default: 3600, null: false
+ end
+end
diff --git a/db/migrate/20151210125929_add_project_id_to_ci.rb b/db/migrate/20151210125929_add_project_id_to_ci.rb
new file mode 100644
index 00000000000..84273591fa2
--- /dev/null
+++ b/db/migrate/20151210125929_add_project_id_to_ci.rb
@@ -0,0 +1,8 @@
+class AddProjectIdToCi < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :gl_project_id, :integer
+ add_column :ci_runner_projects, :gl_project_id, :integer
+ add_column :ci_triggers, :gl_project_id, :integer
+ add_column :ci_variables, :gl_project_id, :integer
+ end
+end
diff --git a/db/migrate/20151210125930_migrate_ci_to_project.rb b/db/migrate/20151210125930_migrate_ci_to_project.rb
new file mode 100644
index 00000000000..c32c7feb193
--- /dev/null
+++ b/db/migrate/20151210125930_migrate_ci_to_project.rb
@@ -0,0 +1,42 @@
+class MigrateCiToProject < ActiveRecord::Migration
+ def up
+ migrate_project_id_for_table('ci_runner_projects')
+ migrate_project_id_for_table('ci_triggers')
+ migrate_project_id_for_table('ci_variables')
+ migrate_project_id_for_builds
+
+ migrate_project_column('id', 'ci_id')
+ migrate_project_column('shared_runners_enabled', 'shared_runners_enabled')
+ migrate_project_column('token', 'runners_token')
+ migrate_project_column('coverage_regex', 'build_coverage_regex')
+ migrate_project_column('allow_git_fetch', 'build_allow_git_fetch')
+ migrate_project_column('timeout', 'build_timeout')
+ migrate_ci_service
+ end
+
+ def down
+ # We can't reverse the data
+ end
+
+ def migrate_project_id_for_table(table)
+ subquery = "SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = #{table}.project_id"
+ execute("UPDATE #{table} SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL")
+ end
+
+ def migrate_project_id_for_builds
+ subquery = 'SELECT gl_project_id FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id'
+ execute("UPDATE ci_builds SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL")
+ end
+
+ def migrate_project_column(column, new_column = nil)
+ new_column ||= column
+ subquery = "SELECT ci_projects.#{column} FROM ci_projects WHERE projects.id = ci_projects.gitlab_id " \
+ 'ORDER BY ci_projects.updated_at DESC LIMIT 1'
+ execute("UPDATE projects SET #{new_column}=(#{subquery}) WHERE (#{subquery}) IS NOT NULL")
+ end
+
+ def migrate_ci_service
+ subquery = "SELECT active FROM services WHERE projects.id = services.project_id AND type='GitlabCiService' LIMIT 1"
+ execute("UPDATE projects SET builds_enabled=(#{subquery}) WHERE (#{subquery}) IS NOT NULL")
+ end
+end
diff --git a/db/migrate/20151210125931_add_index_to_ci_tables.rb b/db/migrate/20151210125931_add_index_to_ci_tables.rb
new file mode 100644
index 00000000000..5e129c9303d
--- /dev/null
+++ b/db/migrate/20151210125931_add_index_to_ci_tables.rb
@@ -0,0 +1,12 @@
+class AddIndexToCiTables < ActiveRecord::Migration
+ def change
+ add_index :ci_builds, :gl_project_id
+ add_index :ci_runner_projects, :gl_project_id
+ add_index :ci_triggers, :gl_project_id
+ add_index :ci_variables, :gl_project_id
+ add_index :projects, :runners_token
+ add_index :projects, :builds_enabled
+ add_index :projects, [:builds_enabled, :shared_runners_enabled]
+ add_index :projects, [:ci_id]
+ end
+end
diff --git a/db/migrate/20151210125932_drop_null_for_ci_tables.rb b/db/migrate/20151210125932_drop_null_for_ci_tables.rb
new file mode 100644
index 00000000000..c520c2ed56f
--- /dev/null
+++ b/db/migrate/20151210125932_drop_null_for_ci_tables.rb
@@ -0,0 +1,9 @@
+class DropNullForCiTables < ActiveRecord::Migration
+ def change
+ remove_index :ci_variables, :project_id
+ remove_index :ci_runner_projects, :project_id
+ change_column_null :ci_triggers, :project_id, true
+ change_column_null :ci_variables, :project_id, true
+ change_column_null :ci_runner_projects, :project_id, true
+ end
+end
diff --git a/db/migrate/20151218154042_add_tfa_to_application_settings.rb b/db/migrate/20151218154042_add_tfa_to_application_settings.rb
new file mode 100644
index 00000000000..dd95db775c5
--- /dev/null
+++ b/db/migrate/20151218154042_add_tfa_to_application_settings.rb
@@ -0,0 +1,8 @@
+class AddTfaToApplicationSettings < ActiveRecord::Migration
+ def change
+ change_table :application_settings do |t|
+ t.boolean :require_two_factor_authentication, default: false
+ t.integer :two_factor_grace_period, default: 48
+ end
+ end
+end
diff --git a/db/migrate/20151221234414_add_tfa_additional_fields.rb b/db/migrate/20151221234414_add_tfa_additional_fields.rb
new file mode 100644
index 00000000000..c16df47932f
--- /dev/null
+++ b/db/migrate/20151221234414_add_tfa_additional_fields.rb
@@ -0,0 +1,7 @@
+class AddTfaAdditionalFields < ActiveRecord::Migration
+ def change
+ change_table :users do |t|
+ t.datetime :otp_grace_period_started_at, null: true
+ end
+ end
+end
diff --git a/db/migrate/20151224123230_rename_emojis.rb b/db/migrate/20151224123230_rename_emojis.rb
new file mode 100644
index 00000000000..62d921dfdcc
--- /dev/null
+++ b/db/migrate/20151224123230_rename_emojis.rb
@@ -0,0 +1,15 @@
+# Migration type: online without errors (works on previous version and new one)
+class RenameEmojis < ActiveRecord::Migration
+ def up
+ # Renames aliases to main names
+ execute("UPDATE notes SET note ='thumbsup' WHERE is_award = true AND note = '+1'")
+ execute("UPDATE notes SET note ='thumbsdown' WHERE is_award = true AND note = '-1'")
+ execute("UPDATE notes SET note ='poop' WHERE is_award = true AND note = 'shit'")
+ end
+
+ def down
+ execute("UPDATE notes SET note ='+1' WHERE is_award = true AND note = 'thumbsup'")
+ execute("UPDATE notes SET note ='-1' WHERE is_award = true AND note = 'thumbsdown'")
+ execute("UPDATE notes SET note ='shit' WHERE is_award = true AND note = 'poop'")
+ end
+end
diff --git a/db/migrate/20151228111122_remove_public_from_namespace.rb b/db/migrate/20151228111122_remove_public_from_namespace.rb
new file mode 100644
index 00000000000..f4c848bbf47
--- /dev/null
+++ b/db/migrate/20151228111122_remove_public_from_namespace.rb
@@ -0,0 +1,6 @@
+# Migration type: online
+class RemovePublicFromNamespace < ActiveRecord::Migration
+ def change
+ remove_column :namespaces, :public, :boolean
+ end
+end
diff --git a/db/migrate/20151228150906_influxdb_settings.rb b/db/migrate/20151228150906_influxdb_settings.rb
new file mode 100644
index 00000000000..3012bd52cfd
--- /dev/null
+++ b/db/migrate/20151228150906_influxdb_settings.rb
@@ -0,0 +1,18 @@
+class InfluxdbSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_enabled, :boolean, default: false
+
+ add_column :application_settings, :metrics_host, :string,
+ default: 'localhost'
+
+ add_column :application_settings, :metrics_database, :string,
+ default: 'gitlab'
+
+ add_column :application_settings, :metrics_username, :string
+ add_column :application_settings, :metrics_password, :string
+ add_column :application_settings, :metrics_pool_size, :integer, default: 16
+ add_column :application_settings, :metrics_timeout, :integer, default: 10
+ add_column :application_settings, :metrics_method_call_threshold,
+ :integer, default: 10
+ end
+end
diff --git a/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
new file mode 100644
index 00000000000..259fd0248d2
--- /dev/null
+++ b/db/migrate/20151228175719_add_recaptcha_to_application_settings.rb
@@ -0,0 +1,9 @@
+class AddRecaptchaToApplicationSettings < ActiveRecord::Migration
+ def change
+ change_table :application_settings do |t|
+ t.boolean :recaptcha_enabled, default: false
+ t.string :recaptcha_site_key
+ t.string :recaptcha_private_key
+ end
+ end
+end
diff --git a/db/migrate/20151229102248_influxdb_udp_port_setting.rb b/db/migrate/20151229102248_influxdb_udp_port_setting.rb
new file mode 100644
index 00000000000..ae0499f936d
--- /dev/null
+++ b/db/migrate/20151229102248_influxdb_udp_port_setting.rb
@@ -0,0 +1,5 @@
+class InfluxdbUdpPortSetting < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_port, :integer, default: 8089
+ end
+end
diff --git a/db/migrate/20151229112614_influxdb_remote_database_setting.rb b/db/migrate/20151229112614_influxdb_remote_database_setting.rb
new file mode 100644
index 00000000000..f0e1ee1e7a7
--- /dev/null
+++ b/db/migrate/20151229112614_influxdb_remote_database_setting.rb
@@ -0,0 +1,5 @@
+class InfluxdbRemoteDatabaseSetting < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :metrics_database
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 0fec00ebf8f..48e6983684a 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,12 +11,12 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20151020173906) do
+ActiveRecord::Schema.define(version: 20151229112614) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
- create_table "abuse_reports", force: true do |t|
+ create_table "abuse_reports", force: :cascade do |t|
t.integer "reporter_id"
t.integer "user_id"
t.text "message"
@@ -24,7 +24,7 @@ ActiveRecord::Schema.define(version: 20151020173906) do
t.datetime "updated_at"
end
- create_table "application_settings", force: true do |t|
+ create_table "application_settings", force: :cascade do |t|
t.integer "default_projects_limit"
t.boolean "signup_enabled"
t.boolean "signin_enabled"
@@ -32,28 +32,44 @@ ActiveRecord::Schema.define(version: 20151020173906) do
t.text "sign_in_text"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "home_page_url"
- t.integer "default_branch_protection", default: 2
- t.boolean "twitter_sharing_enabled", default: true
+ t.string "home_page_url", limit: 255
+ t.integer "default_branch_protection", default: 2
+ t.boolean "twitter_sharing_enabled", default: true
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.string "after_sign_out_path"
- t.integer "session_expire_delay", default: 10080, null: false
+ t.boolean "user_oauth_applications", default: true
+ t.string "after_sign_out_path", limit: 255
+ t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
- t.string "admin_notification_email"
- end
-
- create_table "audit_events", force: true do |t|
- t.integer "author_id", null: false
- t.string "type", null: false
- t.integer "entity_id", null: false
- t.string "entity_type", null: false
+ t.string "admin_notification_email", limit: 255
+ 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.string "metrics_username"
+ t.string "metrics_password"
+ t.integer "metrics_pool_size", default: 16
+ t.integer "metrics_timeout", default: 10
+ t.integer "metrics_method_call_threshold", default: 10
+ t.boolean "recaptcha_enabled", default: false
+ t.string "recaptcha_site_key"
+ t.string "recaptcha_private_key"
+ t.integer "metrics_port", default: 8089
+ end
+
+ create_table "audit_events", force: :cascade do |t|
+ t.integer "author_id", null: false
+ t.string "type", limit: 255, null: false
+ t.integer "entity_id", null: false
+ t.string "entity_type", limit: 255, null: false
t.text "details"
t.datetime "created_at"
t.datetime "updated_at"
@@ -63,27 +79,27 @@ ActiveRecord::Schema.define(version: 20151020173906) 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 "broadcast_messages", force: true do |t|
- t.text "message", null: false
+ create_table "broadcast_messages", force: :cascade do |t|
+ t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.integer "alert_type"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "color"
- t.string "font"
+ t.string "color", limit: 255
+ t.string "font", limit: 255
end
- create_table "ci_application_settings", force: true do |t|
+ create_table "ci_application_settings", force: :cascade do |t|
t.boolean "all_broken_builds"
t.boolean "add_pusher"
t.datetime "created_at"
t.datetime "updated_at"
end
- create_table "ci_builds", force: true do |t|
+ create_table "ci_builds", force: :cascade do |t|
t.integer "project_id"
- t.string "status"
+ t.string "status", limit: 255
t.datetime "finished_at"
t.text "trace"
t.datetime "created_at"
@@ -94,19 +110,21 @@ ActiveRecord::Schema.define(version: 20151020173906) do
t.integer "commit_id"
t.text "commands"
t.integer "job_id"
- t.string "name"
- t.boolean "deploy", default: false
+ t.string "name", limit: 255
+ t.boolean "deploy", default: false
t.text "options"
- t.boolean "allow_failure", default: false, null: false
- t.string "stage"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage", limit: 255
t.integer "trigger_request_id"
t.integer "stage_idx"
t.boolean "tag"
- t.string "ref"
+ t.string "ref", limit: 255
t.integer "user_id"
- t.string "type"
- t.string "target_url"
- t.string "description"
+ t.string "type", limit: 255
+ t.string "target_url", limit: 255
+ t.string "description", limit: 255
+ t.text "artifacts_file"
+ t.integer "gl_project_id"
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
@@ -114,21 +132,22 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
+ add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
add_index "ci_builds", ["type"], name: "index_ci_builds_on_type", using: :btree
- create_table "ci_commits", force: true do |t|
+ create_table "ci_commits", force: :cascade do |t|
t.integer "project_id"
- t.string "ref"
- t.string "sha"
- t.string "before_sha"
+ t.string "ref", limit: 255
+ t.string "sha", limit: 255
+ t.string "before_sha", limit: 255
t.text "push_data"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "tag", default: false
+ t.boolean "tag", default: false
t.text "yaml_errors"
t.datetime "committed_at"
t.integer "gl_project_id"
@@ -141,7 +160,7 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
- create_table "ci_events", force: true do |t|
+ create_table "ci_events", force: :cascade do |t|
t.integer "project_id"
t.integer "user_id"
t.integer "is_admin"
@@ -154,88 +173,89 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
- create_table "ci_jobs", force: true do |t|
- t.integer "project_id", null: false
+ create_table "ci_jobs", force: :cascade do |t|
+ t.integer "project_id", null: false
t.text "commands"
- t.boolean "active", default: true, null: false
+ t.boolean "active", default: true, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "name"
- t.boolean "build_branches", default: true, null: false
- t.boolean "build_tags", default: false, null: false
- t.string "job_type", default: "parallel"
- t.string "refs"
+ t.string "name", limit: 255
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", limit: 255, default: "parallel"
+ t.string "refs", limit: 255
t.datetime "deleted_at"
end
add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
- create_table "ci_projects", force: true do |t|
- t.string "name"
- t.integer "timeout", default: 3600, null: false
+ create_table "ci_projects", force: :cascade do |t|
+ t.string "name", limit: 255
+ t.integer "timeout", default: 3600, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.string "token"
- t.string "default_ref"
- t.string "path"
- t.boolean "always_build", default: false, null: false
+ t.string "token", limit: 255
+ t.string "default_ref", limit: 255
+ t.string "path", limit: 255
+ t.boolean "always_build", default: false, null: false
t.integer "polling_interval"
- t.boolean "public", default: false, null: false
- t.string "ssh_url_to_repo"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo", limit: 255
t.integer "gitlab_id"
- t.boolean "allow_git_fetch", default: true, null: false
- t.string "email_recipients", default: "", null: false
- t.boolean "email_add_pusher", default: true, null: false
- t.boolean "email_only_broken_builds", default: true, null: false
- t.string "skip_refs"
- t.string "coverage_regex"
- t.boolean "shared_runners_enabled", default: false
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", limit: 255, default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs", limit: 255
+ t.string "coverage_regex", limit: 255
+ t.boolean "shared_runners_enabled", default: false
t.text "generated_yaml_config"
end
add_index "ci_projects", ["gitlab_id"], name: "index_ci_projects_on_gitlab_id", using: :btree
add_index "ci_projects", ["shared_runners_enabled"], name: "index_ci_projects_on_shared_runners_enabled", using: :btree
- create_table "ci_runner_projects", force: true do |t|
- t.integer "runner_id", null: false
- t.integer "project_id", null: false
+ create_table "ci_runner_projects", force: :cascade do |t|
+ t.integer "runner_id", null: false
+ t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "gl_project_id"
end
- add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
+ add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
- create_table "ci_runners", force: true do |t|
- t.string "token"
+ create_table "ci_runners", force: :cascade do |t|
+ t.string "token", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
- t.string "description"
+ t.string "description", limit: 255
t.datetime "contacted_at"
- t.boolean "active", default: true, null: false
- t.boolean "is_shared", default: false
- t.string "name"
- t.string "version"
- t.string "revision"
- t.string "platform"
- t.string "architecture"
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name", limit: 255
+ t.string "version", limit: 255
+ t.string "revision", limit: 255
+ t.string "platform", limit: 255
+ t.string "architecture", limit: 255
end
- create_table "ci_services", force: true do |t|
- t.string "type"
- t.string "title"
- t.integer "project_id", null: false
+ create_table "ci_services", force: :cascade do |t|
+ t.string "type", limit: 255
+ t.string "title", limit: 255
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.boolean "active", default: false, null: false
t.text "properties"
end
add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
- create_table "ci_sessions", force: true do |t|
- t.string "session_id", null: false
+ create_table "ci_sessions", force: :cascade do |t|
+ t.string "session_id", limit: 255, null: false
t.text "data"
t.datetime "created_at"
t.datetime "updated_at"
@@ -244,12 +264,12 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
- create_table "ci_taggings", force: true do |t|
+ create_table "ci_taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
- t.string "taggable_type"
+ t.string "taggable_type", limit: 255
t.integer "tagger_id"
- t.string "tagger_type"
+ t.string "tagger_type", limit: 255
t.string "context", limit: 128
t.datetime "created_at"
end
@@ -257,14 +277,14 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
- create_table "ci_tags", force: true do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
+ create_table "ci_tags", force: :cascade do |t|
+ t.string "name", limit: 255
+ t.integer "taggings_count", default: 0
end
add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
- create_table "ci_trigger_requests", force: true do |t|
+ create_table "ci_trigger_requests", force: :cascade do |t|
t.integer "trigger_id", null: false
t.text "variables"
t.datetime "created_at"
@@ -272,35 +292,38 @@ ActiveRecord::Schema.define(version: 20151020173906) do
t.integer "commit_id"
end
- create_table "ci_triggers", force: true do |t|
- t.string "token"
- t.integer "project_id", null: false
+ create_table "ci_triggers", force: :cascade do |t|
+ t.string "token", limit: 255
+ t.integer "project_id"
t.datetime "deleted_at"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "gl_project_id"
end
add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
+ add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
- create_table "ci_variables", force: true do |t|
- t.integer "project_id", null: false
- t.string "key"
+ create_table "ci_variables", force: :cascade do |t|
+ t.integer "project_id"
+ t.string "key", limit: 255
t.text "value"
t.text "encrypted_value"
- t.string "encrypted_value_salt"
- t.string "encrypted_value_iv"
+ t.string "encrypted_value_salt", limit: 255
+ t.string "encrypted_value_iv", limit: 255
+ t.integer "gl_project_id"
end
- add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+ add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
- create_table "ci_web_hooks", force: true do |t|
- t.string "url", null: false
- t.integer "project_id", null: false
+ create_table "ci_web_hooks", force: :cascade do |t|
+ t.string "url", limit: 255, null: false
+ t.integer "project_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
- create_table "deploy_keys_projects", force: true do |t|
+ create_table "deploy_keys_projects", force: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
t.datetime "created_at"
@@ -309,9 +332,9 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "deploy_keys_projects", ["project_id"], name: "index_deploy_keys_projects_on_project_id", using: :btree
- create_table "emails", force: true do |t|
- t.integer "user_id", null: false
- t.string "email", null: false
+ create_table "emails", force: :cascade do |t|
+ t.integer "user_id", null: false
+ t.string "email", limit: 255, null: false
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -319,10 +342,10 @@ ActiveRecord::Schema.define(version: 20151020173906) 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 "events", force: true do |t|
- t.string "target_type"
+ create_table "events", force: :cascade do |t|
+ t.string "target_type", limit: 255
t.integer "target_id"
- t.string "title"
+ t.string "title", limit: 255
t.text "data"
t.integer "project_id"
t.datetime "created_at"
@@ -338,7 +361,7 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "events", ["target_id"], name: "index_events_on_target_id", using: :btree
add_index "events", ["target_type"], name: "index_events_on_target_type", using: :btree
- create_table "forked_project_links", force: true do |t|
+ create_table "forked_project_links", force: :cascade do |t|
t.integer "forked_to_project_id", null: false
t.integer "forked_from_project_id", null: false
t.datetime "created_at"
@@ -347,9 +370,9 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "forked_project_links", ["forked_to_project_id"], name: "index_forked_project_links_on_forked_to_project_id", unique: true, using: :btree
- create_table "identities", force: true do |t|
- t.string "extern_uid"
- t.string "provider"
+ create_table "identities", force: :cascade do |t|
+ t.string "extern_uid", limit: 255
+ t.string "provider", limit: 255
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
@@ -358,18 +381,18 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "identities", ["created_at", "id"], name: "index_identities_on_created_at_and_id", using: :btree
add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree
- create_table "issues", force: true do |t|
- t.string "title"
+ create_table "issues", force: :cascade do |t|
+ t.string "title", limit: 255
t.integer "assignee_id"
t.integer "author_id"
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "position", default: 0
- t.string "branch_name"
+ t.integer "position", default: 0
+ t.string "branch_name", limit: 255
t.text "description"
t.integer "milestone_id"
- t.string "state"
+ t.string "state", limit: 255
t.integer "iid"
t.integer "updated_by_id"
end
@@ -381,26 +404,27 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
+ add_index "issues", ["state"], name: "index_issues_on_state", using: :btree
add_index "issues", ["title"], name: "index_issues_on_title", using: :btree
- create_table "keys", force: true do |t|
+ create_table "keys", force: :cascade do |t|
t.integer "user_id"
t.datetime "created_at"
t.datetime "updated_at"
t.text "key"
- t.string "title"
- t.string "type"
- t.string "fingerprint"
- t.boolean "public", default: false, null: false
+ t.string "title", limit: 255
+ t.string "type", limit: 255
+ t.string "fingerprint", limit: 255
+ t.boolean "public", default: false, null: false
end
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
- create_table "label_links", force: true do |t|
+ create_table "label_links", force: :cascade do |t|
t.integer "label_id"
t.integer "target_id"
- t.string "target_type"
+ t.string "target_type", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
end
@@ -408,29 +432,48 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "label_links", ["label_id"], name: "index_label_links_on_label_id", using: :btree
add_index "label_links", ["target_id", "target_type"], name: "index_label_links_on_target_id_and_target_type", using: :btree
- create_table "labels", force: true do |t|
- t.string "title"
- t.string "color"
+ create_table "labels", force: :cascade do |t|
+ t.string "title", limit: 255
+ t.string "color", limit: 255
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
+ t.boolean "template", default: false
end
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
- create_table "members", force: true do |t|
- t.integer "access_level", null: false
- t.integer "source_id", null: false
- t.string "source_type", null: false
+ create_table "lfs_objects", force: :cascade do |t|
+ t.string "oid", limit: 255, null: false
+ t.integer "size", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "file", limit: 255
+ end
+
+ add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", unique: true, using: :btree
+
+ create_table "lfs_objects_projects", force: :cascade do |t|
+ t.integer "lfs_object_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
+
+ create_table "members", force: :cascade do |t|
+ t.integer "access_level", null: false
+ t.integer "source_id", null: false
+ t.string "source_type", limit: 255, null: false
t.integer "user_id"
- t.integer "notification_level", null: false
- t.string "type"
+ t.integer "notification_level", null: false
+ t.string "type", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.integer "created_by_id"
- t.string "invite_email"
- t.string "invite_token"
+ t.string "invite_email", limit: 255
+ t.string "invite_token", limit: 255
t.datetime "invite_accepted_at"
end
@@ -441,36 +484,39 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
- create_table "merge_request_diffs", force: true do |t|
- t.string "state"
+ create_table "merge_request_diffs", force: :cascade do |t|
+ t.string "state", limit: 255
t.text "st_commits"
t.text "st_diffs"
- t.integer "merge_request_id", null: false
+ t.integer "merge_request_id", null: false
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
- create_table "merge_requests", force: true do |t|
- t.string "target_branch", null: false
- t.string "source_branch", null: false
- t.integer "source_project_id", null: false
+ create_table "merge_requests", force: :cascade do |t|
+ t.string "target_branch", limit: 255, null: false
+ t.string "source_branch", limit: 255, null: false
+ t.integer "source_project_id", null: false
t.integer "author_id"
t.integer "assignee_id"
- t.string "title"
+ t.string "title", limit: 255
t.datetime "created_at"
t.datetime "updated_at"
t.integer "milestone_id"
- t.string "state"
- t.string "merge_status"
- t.integer "target_project_id", null: false
+ t.string "state", limit: 255
+ t.string "merge_status", limit: 255
+ t.integer "target_project_id", null: false
t.integer "iid"
t.text "description"
- t.integer "position", default: 0
+ t.integer "position", default: 0
t.datetime "locked_at"
t.integer "updated_by_id"
- t.string "merge_error"
+ t.string "merge_error", limit: 255
+ t.text "merge_params"
+ t.boolean "merge_when_build_succeeds", default: false, null: false
+ t.integer "merge_user_id"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -484,14 +530,14 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
- create_table "milestones", force: true do |t|
- t.string "title", null: false
- t.integer "project_id", null: false
+ create_table "milestones", force: :cascade do |t|
+ t.string "title", limit: 255, null: false
+ t.integer "project_id", null: false
t.text "description"
t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "state"
+ t.string "state", limit: 255
t.integer "iid"
end
@@ -500,15 +546,15 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
- create_table "namespaces", force: true do |t|
- t.string "name", null: false
- t.string "path", null: false
+ create_table "namespaces", force: :cascade do |t|
+ t.string "name", limit: 255, null: false
+ t.string "path", limit: 255, null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type"
- t.string "description", default: "", null: false
- t.string "avatar"
+ t.string "type", limit: 255
+ t.string "description", limit: 255, default: "", null: false
+ t.string "avatar", limit: 255
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
@@ -517,26 +563,28 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
- create_table "notes", force: true do |t|
+ create_table "notes", force: :cascade do |t|
t.text "note"
- t.string "noteable_type"
+ t.string "noteable_type", limit: 255
t.integer "author_id"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "project_id"
- t.string "attachment"
- t.string "line_code"
- t.string "commit_id"
+ t.string "attachment", limit: 255
+ t.string "line_code", limit: 255
+ t.string "commit_id", limit: 255
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
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", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
add_index "notes", ["noteable_type"], name: "index_notes_on_noteable_type", using: :btree
@@ -544,140 +592,167 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "notes", ["project_id"], name: "index_notes_on_project_id", using: :btree
add_index "notes", ["updated_at"], name: "index_notes_on_updated_at", using: :btree
- create_table "oauth_access_grants", force: true do |t|
- t.integer "resource_owner_id", null: false
- t.integer "application_id", null: false
- t.string "token", null: false
- t.integer "expires_in", null: false
- t.text "redirect_uri", null: false
- t.datetime "created_at", null: false
+ create_table "oauth_access_grants", force: :cascade do |t|
+ t.integer "resource_owner_id", null: false
+ t.integer "application_id", null: false
+ t.string "token", limit: 255, null: false
+ t.integer "expires_in", null: false
+ t.text "redirect_uri", null: false
+ t.datetime "created_at", null: false
t.datetime "revoked_at"
- t.string "scopes"
+ t.string "scopes", limit: 255
end
add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree
- create_table "oauth_access_tokens", force: true do |t|
+ create_table "oauth_access_tokens", force: :cascade do |t|
t.integer "resource_owner_id"
t.integer "application_id"
- t.string "token", null: false
- t.string "refresh_token"
+ t.string "token", limit: 255, null: false
+ t.string "refresh_token", limit: 255
t.integer "expires_in"
t.datetime "revoked_at"
- t.datetime "created_at", null: false
- t.string "scopes"
+ t.datetime "created_at", null: false
+ t.string "scopes", limit: 255
end
add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree
add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree
add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree
- create_table "oauth_applications", force: true do |t|
- t.string "name", null: false
- t.string "uid", null: false
- t.string "secret", null: false
- t.text "redirect_uri", null: false
- t.string "scopes", default: "", null: false
+ create_table "oauth_applications", force: :cascade do |t|
+ t.string "name", limit: 255, null: false
+ t.string "uid", limit: 255, null: false
+ t.string "secret", limit: 255, null: false
+ t.text "redirect_uri", null: false
+ t.string "scopes", limit: 255, default: "", null: false
t.datetime "created_at"
t.datetime "updated_at"
t.integer "owner_id"
- t.string "owner_type"
+ t.string "owner_type", limit: 255
end
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
- create_table "project_import_data", force: true do |t|
+ create_table "project_import_data", force: :cascade do |t|
t.integer "project_id"
t.text "data"
end
- create_table "projects", force: true do |t|
- t.string "name"
- t.string "path"
+ create_table "projects", force: :cascade do |t|
+ t.string "name", limit: 255
+ t.string "path", limit: 255
t.text "description"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "wall_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 "wall_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.string "issues_tracker", limit: 255, default: "gitlab", null: false
+ t.string "issues_tracker_id", limit: 255
+ 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.string "avatar"
- t.string "import_status"
- 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
- end
-
+ t.string "import_url", limit: 255
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
+ t.string "avatar", limit: 255
+ t.string "import_status", limit: 255
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
+ t.string "import_type", limit: 255
+ t.string "import_source", limit: 255
+ 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.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
+ end
+
+ add_index "projects", ["builds_enabled", "shared_runners_enabled"], name: "index_projects_on_builds_enabled_and_shared_runners_enabled", using: :btree
+ add_index "projects", ["builds_enabled"], name: "index_projects_on_builds_enabled", using: :btree
+ add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
add_index "projects", ["created_at", "id"], name: "index_projects_on_created_at_and_id", using: :btree
add_index "projects", ["creator_id"], name: "index_projects_on_creator_id", using: :btree
add_index "projects", ["last_activity_at"], name: "index_projects_on_last_activity_at", using: :btree
add_index "projects", ["namespace_id"], name: "index_projects_on_namespace_id", using: :btree
+ add_index "projects", ["path"], name: "index_projects_on_path", using: :btree
+ add_index "projects", ["runners_token"], name: "index_projects_on_runners_token", using: :btree
add_index "projects", ["star_count"], name: "index_projects_on_star_count", using: :btree
+ add_index "projects", ["visibility_level"], name: "index_projects_on_visibility_level", using: :btree
- create_table "protected_branches", force: true do |t|
- t.integer "project_id", null: false
- t.string "name", null: false
+ create_table "protected_branches", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.string "name", limit: 255, null: false
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "developers_can_push", default: false, null: false
+ t.boolean "developers_can_push", default: false, null: false
end
add_index "protected_branches", ["project_id"], name: "index_protected_branches_on_project_id", using: :btree
- create_table "sent_notifications", force: true do |t|
+ create_table "releases", force: :cascade do |t|
+ t.string "tag", limit: 255
+ t.text "description"
+ t.integer "project_id"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
+ add_index "releases", ["project_id"], name: "index_releases_on_project_id", using: :btree
+
+ create_table "sent_notifications", force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
- t.string "noteable_type"
+ t.string "noteable_type", limit: 255
t.integer "recipient_id"
- t.string "commit_id"
- t.string "reply_key", null: false
- t.string "line_code"
+ t.string "commit_id", limit: 255
+ t.string "reply_key", limit: 255, null: false
+ t.string "line_code", limit: 255
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
- create_table "services", force: true do |t|
- t.string "type"
- t.string "title"
+ create_table "services", force: :cascade do |t|
+ t.string "type", limit: 255
+ t.string "title", limit: 255
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.boolean "active", default: false, null: false
t.text "properties"
- t.boolean "template", default: false
- t.boolean "push_events", default: true
- t.boolean "issues_events", default: true
- t.boolean "merge_requests_events", default: true
- t.boolean "tag_push_events", default: true
- t.boolean "note_events", default: true, null: false
+ t.boolean "template", default: false
+ t.boolean "push_events", default: true
+ t.boolean "issues_events", default: true
+ t.boolean "merge_requests_events", default: true
+ t.boolean "tag_push_events", default: true
+ t.boolean "note_events", default: true, null: false
+ t.boolean "build_events", default: false, null: false
end
add_index "services", ["created_at", "id"], name: "index_services_on_created_at_and_id", using: :btree
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
+ add_index "services", ["template"], name: "index_services_on_template", using: :btree
- create_table "snippets", force: true do |t|
- t.string "title"
+ create_table "snippets", force: :cascade do |t|
+ t.string "title", limit: 255
t.text "content"
- t.integer "author_id", null: false
+ t.integer "author_id", null: false
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "file_name"
+ t.string "file_name", limit: 255
t.datetime "expires_at"
- t.string "type"
- t.integer "visibility_level", default: 0, null: false
+ t.string "type", limit: 255
+ t.integer "visibility_level", default: 0, null: false
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -687,10 +762,10 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
- create_table "subscriptions", force: true do |t|
+ create_table "subscriptions", force: :cascade do |t|
t.integer "user_id"
t.integer "subscribable_id"
- t.string "subscribable_type"
+ t.string "subscribable_type", limit: 255
t.boolean "subscribed"
t.datetime "created_at"
t.datetime "updated_at"
@@ -698,80 +773,83 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
- create_table "taggings", force: true do |t|
+ create_table "taggings", force: :cascade do |t|
t.integer "tag_id"
t.integer "taggable_id"
- t.string "taggable_type"
+ t.string "taggable_type", limit: 255
t.integer "tagger_id"
- t.string "tagger_type"
- t.string "context"
+ t.string "tagger_type", limit: 255
+ t.string "context", limit: 255
t.datetime "created_at"
end
add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
- create_table "tags", force: true do |t|
- t.string "name"
- t.integer "taggings_count", default: 0
+ create_table "tags", force: :cascade do |t|
+ t.string "name", limit: 255
+ t.integer "taggings_count", default: 0
end
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
- create_table "users", force: true do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
- t.string "reset_password_token"
+ create_table "users", force: :cascade do |t|
+ t.string "email", limit: 255, default: "", null: false
+ t.string "encrypted_password", limit: 255, default: "", null: false
+ t.string "reset_password_token", limit: 255
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
- t.integer "sign_in_count", default: 0
+ t.integer "sign_in_count", default: 0
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
- t.string "current_sign_in_ip"
- t.string "last_sign_in_ip"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "name"
- t.boolean "admin", default: false, null: false
- t.integer "projects_limit", default: 10
- t.string "skype", default: "", null: false
- t.string "linkedin", default: "", null: false
- t.string "twitter", default: "", null: false
- t.string "authentication_token"
- t.integer "theme_id", default: 1, null: false
- t.string "bio"
- t.integer "failed_attempts", default: 0
+ t.string "current_sign_in_ip", limit: 255
+ t.string "last_sign_in_ip", limit: 255
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name", limit: 255
+ t.boolean "admin", default: false, null: false
+ t.integer "projects_limit", default: 10
+ t.string "skype", limit: 255, default: "", null: false
+ t.string "linkedin", limit: 255, default: "", null: false
+ t.string "twitter", limit: 255, default: "", null: false
+ t.string "authentication_token", limit: 255
+ t.integer "theme_id", default: 1, null: false
+ t.string "bio", limit: 255
+ t.integer "failed_attempts", default: 0
t.datetime "locked_at"
- t.string "username"
- t.boolean "can_create_group", default: true, null: false
- 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.string "username", limit: 255
+ t.boolean "can_create_group", default: true, null: false
+ t.boolean "can_create_team", default: true, null: false
+ t.string "state", limit: 255
+ 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"
- t.string "avatar"
- t.string "confirmation_token"
+ t.string "avatar", limit: 255
+ t.string "confirmation_token", limit: 255
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
- t.string "unconfirmed_email"
- t.boolean "hide_no_ssh_key", default: false
- t.string "website_url", default: "", null: false
- t.string "notification_email"
- t.boolean "hide_no_password", default: false
- t.boolean "password_automatically_set", default: false
- t.string "location"
- t.string "encrypted_otp_secret"
- t.string "encrypted_otp_secret_iv"
- t.string "encrypted_otp_secret_salt"
- t.boolean "otp_required_for_login", default: false, null: false
+ t.string "unconfirmed_email", limit: 255
+ t.boolean "hide_no_ssh_key", default: false
+ t.string "website_url", limit: 255, default: "", null: false
+ t.string "notification_email", limit: 255
+ t.boolean "hide_no_password", default: false
+ t.boolean "password_automatically_set", default: false
+ t.string "location", limit: 255
+ t.string "encrypted_otp_secret", limit: 255
+ t.string "encrypted_otp_secret_iv", limit: 255
+ t.string "encrypted_otp_secret_salt", limit: 255
+ t.boolean "otp_required_for_login", default: false, null: false
t.text "otp_backup_codes"
- t.string "public_email", default: "", null: false
- t.integer "dashboard", default: 0
- t.integer "project_view", default: 0
+ t.string "public_email", limit: 255, default: "", null: false
+ t.integer "dashboard", default: 0
+ t.integer "project_view", default: 0
t.integer "consumed_timestep"
- t.integer "layout", default: 0
+ t.integer "layout", default: 0
+ t.boolean "hide_project_limit", default: false
+ t.string "unlock_token"
+ t.datetime "otp_grace_period_started_at"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -784,7 +862,7 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree
add_index "users", ["username"], name: "index_users_on_username", using: :btree
- create_table "users_star_projects", force: true do |t|
+ create_table "users_star_projects", force: :cascade do |t|
t.integer "project_id", null: false
t.integer "user_id", null: false
t.datetime "created_at"
@@ -795,19 +873,20 @@ ActiveRecord::Schema.define(version: 20151020173906) do
add_index "users_star_projects", ["user_id", "project_id"], name: "index_users_star_projects_on_user_id_and_project_id", unique: true, using: :btree
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
- create_table "web_hooks", force: true do |t|
- t.string "url"
+ create_table "web_hooks", force: :cascade do |t|
+ t.string "url", limit: 255
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
+ t.string "type", limit: 255, default: "ProjectHook"
t.integer "service_id"
- t.boolean "push_events", default: true, null: false
- t.boolean "issues_events", default: false, null: false
- t.boolean "merge_requests_events", default: false, null: false
- t.boolean "tag_push_events", default: false
- t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: true
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ t.boolean "note_events", default: false, null: false
+ t.boolean "enable_ssl_verification", default: true
+ t.boolean "build_events", default: false, null: false
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
diff --git a/doc/README.md b/doc/README.md
index a0ff856ebb6..f4553a899d3 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -7,6 +7,7 @@
- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
@@ -17,20 +18,35 @@
## CI Documentation
-+ [Quick Start](ci/quick_start/README.md)
-+ [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
-+ [Configuring runner](ci/runners/README.md)
-+ [Configuring deployment](ci/deployment/README.md)
-+ [Using Docker Images](ci/docker/using_docker_images.md)
-+ [Using Docker Build](ci/docker/using_docker_build.md)
-+ [Using Variables](ci/variables/README.md)
+- [Quick Start](ci/quick_start/README.md)
+- [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
+- [Configuring runner](ci/runners/README.md)
+- [Configuring deployment](ci/deployment/README.md)
+- [Using Docker Images](ci/docker/using_docker_images.md)
+- [Using Docker Build](ci/docker/using_docker_build.md)
+- [Using Variables](ci/variables/README.md)
+- [Using SSH keys](ci/ssh_keys/README.md)
+- [User permissions](ci/permissions/README.md)
+- [API](ci/api/README.md)
+- [Triggering builds through the API](ci/triggers/README.md)
+
+### CI Languages
+
+- [Testing PHP](ci/languages/php.md)
+
+### CI Services
+
+- [Using MySQL](ci/services/mysql.md)
+- [Using PostgreSQL](ci/services/postgres.md)
+- [Using Redis](ci/services/redis.md)
+- [Using Other Services](ci/docker/using_docker_images.md#how-to-use-other-images-as-services)
### CI Examples
-+ [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
-+ [Test Clojure applications](ci/examples/test-clojure-application.md)
-+ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
+- [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
+- [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
+- [Test Clojure applications](ci/examples/test-clojure-application.md)
+- Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
## Administrator documentation
@@ -40,6 +56,7 @@
- [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.
+- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
@@ -48,11 +65,7 @@
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
-
-### Administrator documentation
-
-+ [User permissions](permissions/permissions.md)
-+ [API](api/README.md)
+- [Git LFS configuration](workflow/lfs/lfs_administration.md)
## Contributor documentation
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
new file mode 100644
index 00000000000..1eb3a74d304
--- /dev/null
+++ b/doc/administration/environment_variables.md
@@ -0,0 +1,53 @@
+# Environment Variables
+
+## Introduction
+
+Commonly people configure GitLab via the gitlab.rb configuration file in the Omnibus package.
+
+But if you prefer to use environment variables we allow that too.
+
+## Supported environment variables
+
+Variable | Type | Explanation
+-------- | ---- | -----------
+GITLAB_ROOT_PASSWORD | string | sets the password for the `root` user on installation
+GITLAB_HOST | url | hostname of the GitLab server includes http or https
+RAILS_ENV | production / development / staging / test | Rails environment
+DATABASE_URL | url | For example: postgresql://localhost/blog_development?pool=5
+GITLAB_EMAIL_FROM | email | Email address used in the "From" field in mails sent by GitLab
+GITLAB_EMAIL_DISPLAY_NAME | string | Name used in the "From" field in mails sent by GitLab
+GITLAB_EMAIL_REPLY_TO | email | Email address used in the "Reply-To" field in mails sent by GitLab
+
+## Complete database variables
+
+As explained in the [Heroku documentation](https://devcenter.heroku.com/articles/rails-database-connection-behavior) the DATABASE_URL doesn't let you set:
+
+- adapter
+- database
+- username
+- password
+- host
+- port
+
+To do so please `cp config/database.yml.env config/database.yml` and use the following variables:
+
+Variable | Default
+--- | ---
+GITLAB_DATABASE_ADAPTER | postgresql
+GITLAB_DATABASE_ENCODING | unicode
+GITLAB_DATABASE_DATABASE | gitlab_#{ENV['RAILS_ENV']
+GITLAB_DATABASE_POOL | 10
+GITLAB_DATABASE_USERNAME | root
+GITLAB_DATABASE_PASSWORD |
+GITLAB_DATABASE_HOST | localhost
+GITLAB_DATABASE_PORT | 5432
+
+## Adding more variables
+
+We welcome merge requests to make more settings configurable via variables.
+Please stick to the naming scheme "GITLAB_#{name 1_settings.rb in upper case}".
+
+## Omnibus configuration
+
+It's possible to preconfigure the GitLab image by adding the environment variable: `GITLAB_OMNIBUS_CONFIG` to docker run command.
+For more information see the ['preconfigure-docker-container' section in the Omnibus documentation](http://doc.gitlab.com/omnibus/docker/#preconfigure-docker-container).
diff --git a/doc/api/README.md b/doc/api/README.md
index 6b8528de50c..25a31b235cc 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -10,6 +10,7 @@
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Commits](commits.md)
+- [Tags](tags.md)
- [Branches](branches.md)
- [Merge Requests](merge_requests.md)
- [Issues](issues.md)
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 9f72adc6ed9..93d62b751e6 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -22,7 +22,8 @@ Parameters:
"author_name": "Dmitriy Zaporozhets",
"author_email": "dzaporozhets@sphereconsultinginc.com",
"created_at": "2012-09-20T11:50:22+03:00",
- "message": "Replace sanitize with escape once"
+ "message": "Replace sanitize with escape once",
+ "allow_failure": false
},
{
"id": "6104942438c14ec7bd21c6cd5bd995272b3faff6",
@@ -31,7 +32,8 @@ Parameters:
"author_name": "randx",
"author_email": "dmitriy.zaporozhets@gmail.com",
"created_at": "2012-09-20T09:06:12+03:00",
- "message": "Sanitize for network graph"
+ "message": "Sanitize for network graph",
+ "allow_failure": false
}
]
```
@@ -186,7 +188,7 @@ Parameters:
"target_url": "http://jenkins/project/url",
"description": "Jenkins success",
"created_at": "2015-10-12T09:47:16.250Z",
- "started_at": "2015-10-12T09:47:16.250Z"",
+ "started_at": "2015-10-12T09:47:16.250Z",
"finished_at": "2015-10-12T09:47:16.262Z",
"author": {
"id": 1,
@@ -226,7 +228,7 @@ POST /projects/:id/statuses/:sha
"target_url": "http://jenkins/project/url",
"description": "Jenkins success",
"created_at": "2015-10-12T09:47:16.250Z",
- "started_at": "2015-10-12T09:47:16.250Z"",
+ "started_at": "2015-10-12T09:47:16.250Z",
"finished_at": "2015-10-12T09:47:16.262Z",
"author": {
"id": 1,
diff --git a/doc/api/groups.md b/doc/api/groups.md
index 0b9f6406d8d..808675d8605 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -1,6 +1,6 @@
# Groups
-## List project groups
+## List groups
Get a list of groups. (As user: my groups, as admin: all groups)
@@ -21,6 +21,70 @@ GET /groups
You can search for groups by name or path, see below.
+
+## List a group's projects
+
+Get a list of projects in this group.
+
+```
+GET /groups/:id/projects
+```
+
+Parameters:
+
+- `archived` (optional) - if passed, limit by archived status
+- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
+- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
+- `search` (optional) - Return list of authorized projects according to a search criteria
+- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
+
+```json
+[
+ {
+ "id": 4,
+ "description": null,
+ "default_branch": "master",
+ "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",
+ "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,
+ "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"
+ },
+ "archived": false,
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
+ }
+]
+```
+
## Details of a group
Get all details of a group.
@@ -186,7 +250,7 @@ To get more (up to 100), pass the following as an argument to the API call:
/groups?per_page=100
```
-And to switch pages add:
+And to switch pages add:
```
/groups?per_page=100&page=2
-``` \ No newline at end of file
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index ffa7f2cdf14..8bc0a67067a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -3,7 +3,7 @@
## List merge requests
Get all merge requests for this project.
-The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
+The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
@@ -100,9 +100,46 @@ Parameters:
}
```
+## Get single MR commits
+
+Get a list of merge request commits.
+
+```
+GET /projects/:id/merge_request/:merge_request_id/commits
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `merge_request_id` (required) - The ID of MR
+
+
+```json
+[
+ {
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "Replace sanitize with escape once",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2012-09-20T11:50:22+03:00",
+ "message": "Replace sanitize with escape once"
+ },
+ {
+ "id": "6104942438c14ec7bd21c6cd5bd995272b3faff6",
+ "short_id": "6104942438c",
+ "title": "Sanitize for network graph",
+ "author_name": "randx",
+ "author_email": "dmitriy.zaporozhets@gmail.com",
+ "created_at": "2012-09-20T09:06:12+03:00",
+ "message": "Sanitize for network graph"
+ }
+]
+```
+
## Get single MR changes
-Shows information about the merge request including its files and changes
+Shows information about the merge request including its files and changes.
```
GET /projects/:id/merge_request/:merge_request_id/changes
@@ -156,7 +193,7 @@ Parameters:
"updated_at": "2015-02-02T19:49:26.013Z",
"due_date": null
},
- "files": [
+ "changes": [
{
"old_path": "VERSION",
"new_path": "VERSION",
@@ -174,7 +211,6 @@ Parameters:
## Create MR
Creates a new merge request.
-
```
POST /projects/:id/merge_requests
```
@@ -292,9 +328,57 @@ PUT /projects/:id/merge_request/:merge_request_id/merge
Parameters:
-- `id` (required) - The ID of a project
-- `merge_request_id` (required) - ID of MR
-- `merge_commit_message` (optional) - Custom merge commit message
+- `id` (required) - The ID of a project
+- `merge_request_id` (required) - ID of MR
+- `merge_commit_message` (optional) - Custom merge commit message
+- `should_remove_source_branch` (optional) - if `true` removes the source branch
+- `merged_when_build_succeeds` (optional) - if `true` the MR is merge when the build succeeds
+
+```json
+{
+ "id": 1,
+ "target_branch": "master",
+ "source_branch": "test1",
+ "project_id": 3,
+ "title": "test1",
+ "state": "merged",
+ "upvotes": 0,
+ "downvotes": 0,
+ "author": {
+ "id": 1,
+ "username": "admin",
+ "email": "admin@example.com",
+ "name": "Administrator",
+ "state": "active",
+ "created_at": "2012-04-29T08:46:00Z"
+ },
+ "assignee": {
+ "id": 1,
+ "username": "admin",
+ "email": "admin@example.com",
+ "name": "Administrator",
+ "state": "active",
+ "created_at": "2012-04-29T08:46:00Z"
+ }
+}
+```
+
+## Cancel Merge When Build Succeeds
+
+If successful you'll get `200 OK`.
+
+If you don't have permissions to accept this merge request - you'll get a 401
+
+If the merge request is already merged or closed - you get 405 and error message 'Method Not Allowed'
+
+In case the merge request is not set to be merged when the build succeeds, you'll also get a 406 error.
+```
+PUT /projects/:id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds
+```
+Parameters:
+
+- `id` (required) - The ID of a project
+- `merge_request_id` (required) - ID of MR
```json
{
diff --git a/doc/api/notes.md b/doc/api/notes.md
index c683cb883d4..d4d63e825ab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -34,7 +34,9 @@ Parameters:
"created_at": "2013-10-02T09:22:45Z",
"system": true,
"upvote": false,
- "downvote": false
+ "downvote": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
},
{
"id": 305,
@@ -49,9 +51,11 @@ Parameters:
"created_at": "2013-09-30T13:46:01Z"
},
"created_at": "2013-10-02T09:56:03Z",
- "system": false,
+ "system": true,
"upvote": false,
- "downvote": false
+ "downvote": false,
+ "noteable_id": 121,
+ "noteable_type": "Issue"
}
]
```
@@ -218,7 +222,12 @@ Parameters:
"state": "active",
"created_at": "2013-09-30T13:46:01Z"
},
- "created_at": "2013-10-02T08:57:14Z"
+ "created_at": "2013-10-02T08:57:14Z",
+ "system": false,
+ "upvote": false,
+ "downvote": false,
+ "noteable_id": 2,
+ "noteable_type": "MergeRequest"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 96485857035..0ca81ffd49e 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -32,7 +32,6 @@ Parameters:
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
```json
[
@@ -59,7 +58,9 @@ Parameters:
"path": "diaspora-client",
"path_with_namespace": "diaspora/diaspora-client",
"issues_enabled": true,
+ "open_issues_count": 1,
"merge_requests_enabled": true,
+ "builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
@@ -100,7 +101,9 @@ Parameters:
"path": "puppet",
"path_with_namespace": "brightbox/puppet",
"issues_enabled": true,
+ "open_issues_count": 1,
"merge_requests_enabled": true,
+ "builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13:46:02Z",
@@ -115,6 +118,16 @@ Parameters:
"path": "brightbox",
"updated_at": "2013-09-30T13:46:02Z"
},
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
"archived": false,
"avatar_url": null
}
@@ -135,7 +148,21 @@ Parameters:
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
+
+### List starred projects
+
+Get a list of projects which are starred by the authenticated user.
+
+```
+GET /projects/starred
+```
+
+Parameters:
+
+- `archived` (optional) - if passed, limit by archived status
+- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
+- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
+- `search` (optional) - Return list of authorized projects according to a search criteria
### List ALL projects
@@ -151,7 +178,6 @@ Parameters:
- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
- `search` (optional) - Return list of authorized projects according to a search criteria
-- `ci_enabled_first` - Return projects ordered by ci_enabled flag. Projects with enabled GitLab CI go first
### Get single project
@@ -190,7 +216,9 @@ Parameters:
"path": "diaspora-project-site",
"path_with_namespace": "diaspora/diaspora-project-site",
"issues_enabled": true,
+ "open_issues_count": 1,
"merge_requests_enabled": true,
+ "builds_enabled": true,
"wiki_enabled": true,
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
@@ -242,9 +270,17 @@ Parameters:
"target_id": 830,
"target_type": "Issue",
"author_id": 1,
- "author_username": "john",
"data": null,
- "target_title": "Public project search field"
+ "target_title": "Public project search field",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "author_username": "root"
},
{
"title": null,
@@ -253,6 +289,14 @@ Parameters:
"target_id": null,
"target_type": null,
"author_id": 1,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
"author_username": "john",
"data": {
"before": "50d4420237a9de7be1304607147aec22e4a14af7",
@@ -289,9 +333,56 @@ Parameters:
"target_id": 840,
"target_type": "Issue",
"author_id": 1,
- "author_username": "john",
"data": null,
- "target_title": "Finish & merge Code search PR"
+ "target_title": "Finish & merge Code search PR",
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "author_username": "root"
+ },
+ {
+ "title": null,
+ "project_id": 15,
+ "action_name": "commented on",
+ "target_id": 1312,
+ "target_type": "Note",
+ "author_id": 1,
+ "data": null,
+ "target_title": null,
+ "created_at": "2015-12-04T10:33:58.089Z",
+ "note": {
+ "id": 1312,
+ "body": "What an awesome day!",
+ "attachment": null,
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "created_at": "2015-12-04T10:33:56.698Z",
+ "system": false,
+ "upvote": false,
+ "downvote": false,
+ "noteable_id": 377,
+ "noteable_type": "Issue"
+ },
+ "author": {
+ "name": "Dmitriy Zaporozhets",
+ "username": "root",
+ "id": 1,
+ "state": "active",
+ "avatar_url": "http://localhost:3000/uploads/user/avatar/1/fox_avatar.png",
+ "web_url": "http://localhost:3000/u/root"
+ },
+ "author_username": "root"
}
]
```
@@ -312,6 +403,7 @@ Parameters:
- `description` (optional) - short project description
- `issues_enabled` (optional)
- `merge_requests_enabled` (optional)
+- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
@@ -331,9 +423,9 @@ Parameters:
- `user_id` (required) - user_id of owner
- `name` (required) - new project name
- `description` (optional) - short project description
-- `default_branch` (optional) - 'master' by default
- `issues_enabled` (optional)
- `merge_requests_enabled` (optional)
+- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
@@ -357,6 +449,7 @@ Parameters:
- `default_branch` (optional)
- `issues_enabled` (optional)
- `merge_requests_enabled` (optional)
+- `builds_enabled` (optional)
- `wiki_enabled` (optional)
- `snippets_enabled` (optional)
- `public` (optional) - if `true` same as setting visibility_level = 20
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 33167453802..b6cca5d4e2a 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -1,79 +1,5 @@
# Repositories
-## List project repository tags
-
-Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
-
-```
-GET /projects/:id/repository/tags
-```
-
-Parameters:
-
-- `id` (required) - The ID of a project
-
-```json
-[
- {
- "commit": {
- "author_name": "John Smith",
- "author_email": "john@example.com",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_name": "Jack Smith",
- "committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parents_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
- },
- "name": "v1.0.0",
- "message": null
- }
-]
-```
-
-## Create a new tag
-
-Creates new tag in the repository that points to the supplied ref.
-
-```
-POST /projects/:id/repository/tags
-```
-
-Parameters:
-
-- `id` (required) - The ID of a project
-- `tag_name` (required) - The name of a tag
-- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
-- `message` (optional) - Creates annotated tag.
-
-```json
-{
- "commit": {
- "author_name": "John Smith",
- "author_email": "john@example.com",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_name": "Jack Smith",
- "committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parents_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
- },
- "name": "v1.0.0",
- "message": null
-}
-```
-The message will be `nil` when creating a lightweight tag otherwise
-it will contain the annotation.
-
-It returns 200 if the operation succeed. In case of an error,
-405 with an explaining error message is returned.
-
## List repository tree
Get a list of repository files and directories in a project.
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 25311b07107..623063f357b 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -23,7 +23,8 @@ Example response:
"content": "IyA9PSBTY2hlbWEgSW5mb3...",
"ref": "master",
"blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
- "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50"
+ "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
+ "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d"
}
```
diff --git a/doc/api/settings.md b/doc/api/settings.md
index d1b93a09c02..96867c67915 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -57,7 +57,7 @@ Parameters:
- `default_project_visibility` - what visibility level new project receives
- `default_snippet_visibility` - what visibility level new snippet receives
- `restricted_signup_domains` - force people to use only corporate emails for signup
-- `user_oauth_applications` - allow users to create oauth applicaitons
+- `user_oauth_applications` - allow users to create oauth applications
- `after_sign_out_path` - where redirect user after logout
All parameters are optional. You can send only one that you want to change.
diff --git a/doc/api/tags.md b/doc/api/tags.md
new file mode 100644
index 00000000000..085d387e824
--- /dev/null
+++ b/doc/api/tags.md
@@ -0,0 +1,131 @@
+# Tags
+
+## List project repository tags
+
+Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
+
+```
+GET /projects/:id/repository/tags
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+
+```json
+[
+ {
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "release": {
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+ },
+ "name": "v1.0.0",
+ "message": null
+ }
+]
+```
+
+## Create a new tag
+
+Creates a new tag in the repository that points to the supplied ref.
+
+```
+POST /projects/:id/repository/tags
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
+- `message` (optional) - Creates annotated tag.
+- `release_description` (optional) - Add release notes to the git tag and store it in the GitLab database.
+
+```json
+{
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "release": {
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+ },
+ "name": "v1.0.0",
+ "message": null
+}
+```
+The message will be `nil` when creating a lightweight tag otherwise
+it will contain the annotation.
+
+It returns 200 if the operation succeed. In case of an error,
+405 with an explaining error message is returned.
+
+
+## Create a new release
+
+Add release notes to the existing git tag. It returns 201 if the release is
+created successfully. If the tag does not exist, 404 is returned. If there
+already exists a release for the given tag, 409 is returned.
+
+```
+POST /projects/:id/repository/tags/:tag_name/release
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `description` (required) - Release notes with markdown support
+
+```json
+{
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+}
+```
+
+## Update a release
+
+Updates the release notes of a given release. It returns 200 if the release is
+successfully updated. If the tag or the release does not exist, it returns 404
+with a proper error message.
+
+```
+PUT /projects/:id/repository/tags/:tag_name/release
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `description` (required) - Release notes with markdown support
+
+```json
+{
+ "tag_name": "1.0.0",
+ "description": "Amazing release. Wow"
+}
+``` \ No newline at end of file
diff --git a/doc/api/users.md b/doc/api/users.md
index 7ba2db248ff..773fe36d277 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -90,7 +90,17 @@ GET /users
You can search for users by email or username with: `/users?search=John`
-Also see `def search query` in `app/models/user.rb`.
+In addition, you can lookup users by username:
+
+```
+GET /users?username=:username
+```
+
+For example:
+
+```
+GET /users?username=jack_smith
+```
## Single user
@@ -113,6 +123,13 @@ Parameters:
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
+ "created_at": "2012-05-23T08:00:58Z",
+ "is_admin": false,
+ "bio": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": ""
}
```
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 97325069ceb..a1f5513d88e 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -2,16 +2,30 @@
### User documentation
-+ [Quick Start](quick_start/README.md)
-+ [Configuring project (.gitlab-ci.yml)](yaml/README.md)
-+ [Configuring runner](runners/README.md)
-+ [Configuring deployment](deployment/README.md)
-+ [Using Docker Images](docker/using_docker_images.md)
-+ [Using Docker Build](docker/using_docker_build.md)
-+ [Using Variables](variables/README.md)
+* [Quick Start](quick_start/README.md)
+* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
+* [Configuring runner](runners/README.md)
+* [Configuring deployment](deployment/README.md)
+* [Using Docker Images](docker/using_docker_images.md)
+* [Using Docker Build](docker/using_docker_build.md)
+* [Using Variables](variables/README.md)
+* [Using SSH keys](ssh_keys/README.md)
+* [Triggering builds through the API](triggers/README.md)
+
+### Languages
+
+* [Testing PHP](languages/php.md)
+
+### Services
+
+* [Using MySQL](services/mysql.md)
+* [Using PostgreSQL](services/postgres.md)
+* [Using Redis](services/redis.md)
+* [Using Other Services](docker/using_docker_images.md#how-to-use-other-images-as-services)
### Examples
++ [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
+ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
+ [Test Clojure applications](examples/test-clojure-application.md)
@@ -19,5 +33,5 @@
### Administrator documentation
-+ [User permissions](permissions/README.md)
-+ [API](api/README.md)
+* [User permissions](permissions/README.md)
+* [API](api/README.md)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
index 33c5b172e98..cf9710ede57 100644
--- a/doc/ci/api/README.md
+++ b/doc/ci/api/README.md
@@ -25,7 +25,7 @@ GitLab CI API has 4 authentication methods:
Authentication is done by
sending the `private-token` of a valid user and the `url` of an
-authorized Gitlab instance via a query string along with the API
+authorized GitLab instance via a query string along with the API
request:
GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md
index 5585191e826..74a4c64d000 100644
--- a/doc/ci/api/projects.md
+++ b/doc/ci/api/projects.md
@@ -1,7 +1,7 @@
# Projects API
This API is intended to aid in the setup and configuration of
-projects on Gitlab CI.
+projects on GitLab CI.
__Authentication is done by GitLab user token & GitLab url__
@@ -88,23 +88,23 @@ authorized.
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
+ * `id` (required) - The ID of the GitLab CI project
### Create Project
-Creates a Gitlab CI project using Gitlab project details.
+Creates a GitLab CI project using GitLab project details.
POST /ci/projects
Parameters:
* `name` (required) - The name of the project
- * `gitlab_id` (required) - The ID of the project on the Gitlab instance
+ * `gitlab_id` (required) - The ID of the project on the GitLab instance
* `default_ref` (optional) - The branch to run on (default to `master`)
### Update Project
-Updates a Gitlab CI project using Gitlab project details that the
+Updates a GitLab CI project using GitLab project details that the
authenticated user has access to.
PUT /ci/projects/:id
@@ -116,13 +116,13 @@ Parameters:
### Remove Project
-Removes a Gitlab CI project that the authenticated user has access to.
+Removes a GitLab CI project that the authenticated user has access to.
DELETE /ci/projects/:id
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
+ * `id` (required) - The ID of the GitLab CI project
### Link Project to Runner
@@ -133,8 +133,8 @@ authorized user).
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
- * `runner_id` (required) - The ID of the Gitlab CI runner
+ * `id` (required) - The ID of the GitLab CI project
+ * `runner_id` (required) - The ID of the GitLab CI runner
### Remove Project from Runner
@@ -145,5 +145,5 @@ via authorized user).
Parameters:
- * `id` (required) - The ID of the Gitlab CI project
- * `runner_id` (required) - The ID of the Gitlab CI runner \ No newline at end of file
+ * `id` (required) - The ID of the GitLab CI project
+ * `runner_id` (required) - The ID of the GitLab CI runner \ No newline at end of file
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
index e9f88ee066e..c383dc4bcc9 100644
--- a/doc/ci/api/runners.md
+++ b/doc/ci/api/runners.md
@@ -6,7 +6,7 @@
__Authentication is done by GitLab user token & GitLab url__
-Used to get information about all runners registered on the Gitlab CI
+Used to get information about all runners registered on the GitLab CI
instance.
GET /ci/runners
@@ -31,7 +31,7 @@ Returns:
__Authentication is done with a Shared runner registration token or a project Specific runner registration token__
-Used to make Gitlab CI aware of available runners.
+Used to make GitLab CI aware of available runners.
POST /ci/runners/register
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 5af27470d2f..4b1788a9af0 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -35,7 +35,7 @@ GitLab Runner then executes build scripts as `gitlab-runner` user.
```bash
$ sudo gitlab-runner register -n \
- --url http://gitlab.com/ci \
+ --url https://gitlab.com/ci \
--token RUNNER_TOKEN \
--executor shell
--description "My Runner"
@@ -84,7 +84,7 @@ In order to do that follow the steps:
```bash
$ sudo gitlab-runner register -n \
- --url http://gitlab.com/ci \
+ --url https://gitlab.com/ci \
--token RUNNER_TOKEN \
--executor docker \
--description "My Docker Runner" \
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 191e3a8144d..31458d61674 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -1,19 +1,29 @@
# Using Docker Images
-GitLab CI can use [Docker Engine](https://www.docker.com/) to build projects.
-Docker is an open-source project that allows to use predefined images to run applications
-in independent "containers" that are run within a single Linux instance.
-[Docker Hub](https://registry.hub.docker.com/) have rich database of built images that can be used to build applications.
+GitLab CI in conjunction with [GitLab Runner](../runners/README.md) can use
+[Docker Engine](https://www.docker.com/) to test and build any application.
-Docker when used with GitLab CI runs each build in separate and isolated container using predefined image and always from scratch.
-It makes it easier to have simple and reproducible build environment that can also be run on your workstation.
-This allows you to test all commands from your shell, rather than having to test them on a CI server.
+Docker is an open-source project that allows you to use predefined images to
+run applications in independent "containers" that are run within a single Linux
+instance. [Docker Hub][hub] has a rich database of pre-built images that can be
+used to test and build your applications.
-### Register Docker runner
-To use GitLab Runner with Docker you need to register new runner to use `docker` executor:
+Docker, when used with GitLab CI, runs each build in a separate and isolated
+container using the predefined image that is set up in
+[`.gitlab-ci.yml`](../yaml/README.md).
+
+This makes it easier to have a simple and reproducible build environment that
+can also run on your workstation. The added benefit is that you can test all
+the commands that we will explore later from your shell, rather than having to
+test them on a dedicated CI server.
+
+## Register docker runner
+
+To use GitLab Runner with docker you need to register a new runner to use the
+`docker` executor:
```bash
-gitlab-ci-multi-runner register \
+gitlab-runner register \
--url "https://gitlab.com/" \
--registration-token "PROJECT_REGISTRATION_TOKEN" \
--description "docker-ruby-2.1" \
@@ -23,101 +33,79 @@ gitlab-ci-multi-runner register \
--docker-mysql latest
```
-**The registered runner will use `ruby:2.1` image and will run two services (`postgres:latest` and `mysql:latest`) that will be accessible for time of the build.**
+The registered runner will use the `ruby:2.1` docker image and will run two
+services, `postgres:latest` and `mysql:latest`, both of which will be
+accessible during the build process.
-### What is image?
-The image is the name of any repository that is present in local Docker Engine or any repository that can be found at [Docker Hub](https://registry.hub.docker.com/).
-For more information about the image and Docker Hub please read the [Docker Fundamentals](https://docs.docker.com/introduction/understanding-docker/).
+## What is image
-### What is service?
-Service is just another image that is run for time of your build and is linked to your build. This allows you to access the service image during build time.
-The service image can run any application, but most common use case is to run some database container, ie.: `mysql`.
-It's easier and faster to use existing image, run it as additional container than install `mysql` every time project is built.
+The `image` keyword is the name of the docker image that is present in the
+local Docker Engine (list all images with `docker images`) or any image that
+can be found at [Docker Hub][hub]. For more information about images and Docker
+Hub please read the [Docker Fundamentals][] documentation.
-#### How is service linked to the build?
-There's good document that describes how Docker linking works: [Linking containers together](https://docs.docker.com/userguide/dockerlinks/).
-To summarize: if you add `mysql` as service to your application, the image will be used to create container that is linked to build container.
-The service container for MySQL will be accessible under hostname `mysql`.
-So, **to access your database service you have to connect to host: `mysql` instead of socket or `localhost`**.
+In short, with `image` we refer to the docker image, which will be used to
+create a container on which your build will run.
-### How to use other images as services?
-You are not limited to have only database services.
-You can hand modify `config.toml` to add any image as service found at [Docker Hub](https://registry.hub.docker.com/).
-Look for `[runners.docker]` section:
-```
-[runners.docker]
- image = "ruby:2.1"
- services = ["mysql:latest", "postgres:latest"]
-```
+## What is service
-For example you need `wordpress` instance to test some API integration with `Wordpress`.
-You can for example use this image: [tutum/wordpress](https://registry.hub.docker.com/u/tutum/wordpress/).
-This is image that have fully preconfigured `wordpress` and have `MySQL` server built-in:
-```
-[runners.docker]
- image = "ruby:2.1"
- services = ["mysql:latest", "postgres:latest", "tutum/wordpress:latest"]
-```
+The `services` keyword defines just another docker image that is run during
+your build and is linked to the docker image that the `image` keyword defines.
+This allows you to access the service image during build time.
-Next time when you run your application the `tutum/wordpress` will be started
-and you will have access to it from your build container under hostname: `tutum_wordpress`.
+The service image can run any application, but the most common use case is to
+run a database container, eg. `mysql`. It's easier and faster to use an
+existing image and run it as an additional container than install `mysql` every
+time the project is built.
-Alias hostname for the service is made from the image name:
-1. Everything after `:` is stripped,
-2. '/' is replaced to `_`.
+You can see some widely used services examples in the relevant documentation of
+[CI services examples](../services/README.md).
-### Configuring services
-Many services accept environment variables, which allow you to easily change database names or set account names depending on the environment.
+### How is service linked to the build
-GitLab Runner 0.5.0 and up passes all YAML-defined variables to created service containers.
+To better understand how the container linking works, read
+[Linking containers together](https://docs.docker.com/userguide/dockerlinks/).
-1. To configure database name for [postgres](https://registry.hub.docker.com/u/library/postgres/) service,
-you need to set POSTGRES_DB.
+To summarize, if you add `mysql` as service to your application, the image will
+then be used to create a container that is linked to the build container.
- ```yaml
- services:
- - postgres
-
- variables:
- POSTGRES_DB: gitlab
- ```
+The service container for MySQL will be accessible under the hostname `mysql`.
+So, in order to access your database service you have to connect to the host
+named `mysql` instead of a socket or `localhost`.
-1. To use [mysql](https://registry.hub.docker.com/u/library/mysql/) service with empty password for time of build,
-you need to set MYSQL_ALLOW_EMPTY_PASSWORD.
+## Overwrite image and services
- ```yaml
- services:
- - mysql
-
- variables:
- MYSQL_ALLOW_EMPTY_PASSWORD: yes
- ```
+See [How to use other images as services](#how-to-use-other-images-as-services).
-For other possible configuration variables check the
-https://registry.hub.docker.com/u/library/mysql/ or https://registry.hub.docker.com/u/library/postgres/
-or README page for any other Docker image.
+## How to use other images as services
-**Note: All variables will passed to all service containers. It's not designed to distinguish which variable should go where.**
+You are not limited to have only database services. You can add as many
+services you need to `.gitlab-ci.yml` or manually modify `config.toml`.
+Any image found at [Docker Hub][hub] can be used as a service.
-### Overwrite image and services
-It's possible to overwrite `docker-image` and specify services from `.gitlab-ci.yml`.
-If you add to your YAML the `image` and the `services` these parameters
-be used instead of the ones that were specified during runner's registration.
-```
+## Define image and services from `.gitlab-ci.yml`
+
+You can simply define an image that will be used for all jobs and a list of
+services that you want to use during build time.
+
+```yaml
image: ruby:2.2
+
services:
- postgres:9.3
-before_install:
+
+before_script:
- bundle install
-
+
test:
script:
- bundle exec rake spec
```
-It's possible to define image and service per-job:
-```
-before_install:
+It is also possible to define different images and services per job:
+
+```yaml
+before_script:
- bundle install
test:2.1:
@@ -135,34 +123,91 @@ test:2.2:
- bundle exec rake spec
```
-#### How to enable overwriting?
-To enable overwriting you have to **enable it first** (it's disabled by default for security reasons).
-You can do that by hand modifying runner configuration: `config.toml`.
-Please go to section where is `[runners.docker]` definition for your runner.
-Add `allowed_images` and `allowed_services` to specify what images are allowed to be picked from `.gitlab-ci.yml`:
+## Define image and services in `config.toml`
+
+Look for the `[runners.docker]` section:
+
```
[runners.docker]
image = "ruby:2.1"
- allowed_images = ["ruby:*", "python:*"]
- allowed_services = ["mysql:*", "redis:*"]
+ services = ["mysql:latest", "postgres:latest"]
```
-This enables you to use in your `.gitlab-ci.yml` any image that matches above wildcards.
-You will be able to pick only `ruby` and `python` images.
-The same rule can be applied to limit services.
-If you are courageous enough, you can make it fully open and accept everything:
+The image and services defined this way will be added to all builds run by
+that runner.
+
+## Define an image from a private Docker registry
+
+Starting with GitLab Runner 0.6.0, you are able to define images located to
+private registries that could also require authentication.
+
+All you have to do is be explicit on the image definition in `.gitlab-ci.yml`.
+
+```yaml
+image: my.registry.tld:5000/namepace/image:tag
```
-[runners.docker]
- image = "ruby:2.1"
- allowed_images = ["*", "*/*"]
- allowed_services = ["*", "*/*"]
+
+In the example above, GitLab Runner will look at `my.registry.tld:5000` for the
+image `namespace/image:tag`.
+
+If the repository is private you need to authenticate your GitLab Runner in the
+registry. Learn how to do that on
+[GitLab Runner's documentation][runner-priv-reg].
+
+## Accessing the services
+
+Let's say that you need a Wordpress instance to test some API integration with
+your application.
+
+You can then use for example the [tutum/wordpress][] image in your
+`.gitlab-ci.yml`:
+
+```yaml
+services:
+- tutum/wordpress:latest
```
-**It the feature is not enabled, or image isn't allowed the error message will be put into the build log.**
+When the build is run, `tutum/wordpress` will be started and you will have
+access to it from your build container under the hostname `tutum__wordpress`.
+
+The alias hostname for the service is made from the image name following these
+rules:
+
+1. Everything after `:` is stripped
+2. Backslash (`/`) is replaced with double underscores (`__`)
+
+## Configuring services
+
+Many services accept environment variables which allow you to easily change
+database names or set account names depending on the environment.
+
+GitLab Runner 0.5.0 and up passes all YAML-defined variables to the created
+service containers.
+
+For all possible configuration variables check the documentation of each image
+provided in their corresponding Docker hub page.
+
+*Note: All variables will be passed to all services containers. It's not
+designed to distinguish which variable should go where.*
+
+### PostgreSQL service example
+
+See the specific documentation for
+[using PostgreSQL as a service](../services/postgres.md).
+
+### MySQL service example
+
+See the specific documentation for
+[using MySQL as a service](../services/mysql.md).
+
+## How Docker integration works
+
+Below is a high level overview of the steps performed by docker during build
+time.
-### How Docker integration works
1. Create any service container: `mysql`, `postgresql`, `mongodb`, `redis`.
-1. Create cache container to store all volumes as defined in `config.toml` and `Dockerfile` of build image (`ruby:2.1` as in above example).
+1. Create cache container to store all volumes as defined in `config.toml` and
+ `Dockerfile` of build image (`ruby:2.1` as in above example).
1. Create build container and link any service container to build container.
1. Start build container and send build script to the container.
1. Run build script.
@@ -171,33 +216,64 @@ If you are courageous enough, you can make it fully open and accept everything:
1. Check exit status of build script.
1. Remove build container and all created service containers.
-### How to debug a build locally
-1. Create a file with build script:
+## How to debug a build locally
+
+*Note: The following commands are run without root privileges. You should be
+able to run docker with your regular user account.*
+
+First start with creating a file named `build script`:
+
```bash
-$ cat <<EOF > build_script
+cat <<EOF > build_script
git clone https://gitlab.com/gitlab-org/gitlab-ci-multi-runner.git /builds/gitlab-org/gitlab-ci-multi-runner
cd /builds/gitlab-org/gitlab-ci-multi-runner
-make <- or any other build step
+make
EOF
```
-1. Create service containers:
+Here we use as an example the GitLab Runner repository which contains a
+Makefile, so running `make` will execute the commands defined in the Makefile.
+Your mileage may vary, so instead of `make` you could run the command which
+is specific to your project.
+
+Then create some service containers:
+
```
-$ docker run -d -n service-mysql mysql:latest
-$ docker run -d -n service-postgres postgres:latest
+docker run -d -n service-mysql mysql:latest
+docker run -d -n service-postgres postgres:latest
```
-This will create two service containers (MySQL and PostgreSQL).
-1. Create a build container and execute script in its context:
+This will create two service containers, named `service-mysql` and
+`service-postgres` which use the latest MySQL and PostgreSQL images
+respectively. They will both run in the background (`-d`).
+
+Finally, create a build container by executing the `build_script` file we
+created earlier:
+
```
-$ cat build_script | docker run -n build -i -l mysql:service-mysql -l postgres:service-postgres ruby:2.1 /bin/bash
+docker run --name build -i --link=service-mysql:mysql --link=service-postgres:postgres ruby:2.1 /bin/bash < build_script
```
-This will create build container that has two service containers linked.
-The build_script is piped using STDIN to bash interpreter which executes the build script in container.
-1. At the end remove all containers:
+The above command will create a container named `build` that is spawned from
+the `ruby:2.1` image and has two services linked to it. The `build_script` is
+piped using STDIN to the bash interpreter which in turn executes the
+`build_script` in the `build` container.
+
+When you finish testing and no longer need the containers, you can remove them
+with:
+
```
docker rm -f -v build service-mysql service-postgres
```
-This will forcefully (the `-f` switch) remove build container and service containers
-and all volumes (the `-v` switch) that were created with the container creation.
+
+This will forcefully (`-f`) remove the `build` container, the two service
+containers as well as all volumes (`-v`) that were created with the container
+creation.
+
+[Docker Fundamentals]: https://docs.docker.com/engine/introduction/understanding-docker/
+[hub]: https://hub.docker.com/
+[linking-containers]: https://docs.docker.com/engine/userguide/networking/default_network/dockerlinks/
+[tutum/wordpress]: https://registry.hub.docker.com/u/tutum/wordpress/
+[postgres-hub]: https://registry.hub.docker.com/u/library/postgres/
+[mysql-hub]: https://registry.hub.docker.com/u/library/mysql/
+[runner-priv-reg]: 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/ci/examples/README.md b/doc/ci/examples/README.md
index e0b9fa0e25d..1cf41aea391 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,5 +1,5 @@
# Build script examples
-+ [Test and deploy Ruby Application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
-+ [Test and deploy Python Application to Heroku](test-and-deploy-python-application-to-heroku.md)
-+ [Test Clojure applications](examples/test-clojure-application.md)
++ [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
++ [Test a Clojure application](test-clojure-application.md)
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 036b03dd6b9..a236da53fe9 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -1,7 +1,7 @@
## Test and Deploy a python application
This example will guide you how to run tests in your Python application and deploy it automatically as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://ci.gitlab.com/projects/4080).
+You can checkout the example [source](https://gitlab.com/ayufan/python-getting-started) and check [CI status](https://gitlab.com/ayufan/python-getting-started/builds?scope=all).
### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
diff --git a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
index d2a872f1934..e52e1547461 100644
--- a/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md
@@ -1,7 +1,7 @@
## Test and Deploy a ruby application
This example will guide you how to run tests in your Ruby application and deploy it automatiacally as Heroku application.
-You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://ci.gitlab.com/projects/4050).
+You can checkout the example [source](https://gitlab.com/ayufan/ruby-getting-started) and check [CI status](https://gitlab.com/ayufan/ruby-getting-started/builds?scope=all).
### Configure project
This is what the `.gitlab-ci.yml` file looks like for this project:
@@ -64,4 +64,4 @@ gitlab-ci-multi-runner register \
With the command above, you create a runner that uses [ruby:2.1](https://registry.hub.docker.com/u/library/ruby/) image and uses [postgres](https://registry.hub.docker.com/u/library/postgres/) database.
-To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password. \ No newline at end of file
+To access PostgreSQL database you need to connect to `host: postgres` as user `postgres` without password.
diff --git a/doc/ci/examples/test-clojure-application.md b/doc/ci/examples/test-clojure-application.md
index eaee94a10f1..56b746ce025 100644
--- a/doc/ci/examples/test-clojure-application.md
+++ b/doc/ci/examples/test-clojure-application.md
@@ -1,8 +1,8 @@
-## Test Clojure applications
+## Test a Clojure application
This example will guide you how to run tests in your Clojure application.
-You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://ci.gitlab.com/projects/6306).
+You can checkout the example [source](https://gitlab.com/dzaporozhets/clojure-web-application) and check [CI status](https://gitlab.com/dzaporozhets/clojure-web-application/builds?scope=all).
### Configure project
diff --git a/doc/ci/img/builds_tab.png b/doc/ci/img/builds_tab.png
new file mode 100644
index 00000000000..d088b8b329d
--- /dev/null
+++ b/doc/ci/img/builds_tab.png
Binary files differ
diff --git a/doc/ci/languages/README.md b/doc/ci/languages/README.md
new file mode 100644
index 00000000000..54b2343e08b
--- /dev/null
+++ b/doc/ci/languages/README.md
@@ -0,0 +1,7 @@
+### Languages
+
+This is a list of languages you can test with GitLab CI. Each section has
+comprehensive documentation and comes with a test repository hosted on
+GitLab.com
+
++ [Testing PHP](php.md)
diff --git a/doc/ci/languages/php.md b/doc/ci/languages/php.md
new file mode 100644
index 00000000000..dacb67fa3ff
--- /dev/null
+++ b/doc/ci/languages/php.md
@@ -0,0 +1,284 @@
+# Testing PHP projects
+
+This guide covers basic building instructions for PHP projects.
+
+There are covered two cases: testing using the Docker executor and testing
+using the Shell executor.
+
+## Test PHP projects using the Docker executor
+
+While it is possible to test PHP apps on any system, this would require manual
+configuration from the developer. To overcome this we will be using the
+official [PHP docker image][php-hub] that can be found in Docker Hub.
+
+This will allow us to test PHP projects against different versions of PHP.
+However, not everything is plug 'n' play, you still need to onfigure some
+things manually.
+
+As with every build, you need to create a valid `.gitlab-ci.yml` describing the
+build environment.
+
+Let's first specify the PHP image that will be used for the build process
+(you can read more about what an image means in the Runner's lingo reading
+about [Using Docker images](../docker/using_docker_images.md#what-is-image)).
+
+Start by adding the image to your `.gitlab-ci.yml`:
+
+```yaml
+image: php:5.6
+```
+
+The official images are great, but they lack a few useful tools for testing.
+We need to first prepare the build environment. A way to overcome this is to
+create a script which installs all prerequisites prior the actual testing is
+done.
+
+Let's create a `ci/docker_install.sh` file in the root directory of our
+repository with the following content:
+
+```bash
+#!/bin/bash
+
+# We need to install dependencies only for Docker
+[[ ! -e /.dockerinit ]] && exit 0
+
+set -xe
+
+# Install git (the php image doesn't have it) which is required by composer
+apt-get update -yqq
+apt-get install git -yqq
+
+# Install phpunit, the tool that we will use for testing
+curl -o /usr/local/bin/phpunit https://phar.phpunit.de/phpunit.phar
+chmod +x /usr/local/bin/phpunit
+
+# Install mysql driver
+# Here you can install any other extension that you need
+docker-php-ext-install pdo_mysql
+```
+
+You might wonder what `docker-php-ext-install` is. In short, it is a script
+provided by the official php docker image that you can use to easilly install
+extensions. For more information read the the documentation at
+<https://hub.docker.com/_/php/>.
+
+Now that we created the script that contains all prerequisites for our build
+environment, let's add it in `.gitlab-ci.yml`:
+
+```yaml
+...
+
+before_script:
+- bash ci/docker_install.sh > /dev/null
+
+...
+```
+
+Last step, run the actual tests using `phpunit`:
+
+```yaml
+...
+
+test:app:
+ script:
+ - phpunit --configuration phpunit_myapp.xml
+
+...
+```
+
+Finally, commit your files and push them to GitLab to see your build succeeding
+(or failing).
+
+The final `.gitlab-ci.yml` should look similar to this:
+
+```yaml
+# Select image from https://hub.docker.com/_/php/
+image: php:5.6
+
+before_script:
+# Install dependencies
+- ci/docker_install.sh > /dev/null
+
+test:app:
+ script:
+ - phpunit --configuration phpunit_myapp.xml
+```
+
+### Test against different PHP versions in Docker builds
+
+Testing against multiple versions of PHP is super easy. Just add another job
+with a different docker image version and the runner will do the rest:
+
+```yaml
+before_script:
+# Install dependencies
+- ci/docker_install.sh > /dev/null
+
+# We test PHP5.6
+test:5.6:
+ image: php:5.6
+ script:
+ - phpunit --configuration phpunit_myapp.xml
+
+# We test PHP7.0 (good luck with that)
+test:7.0:
+ image: php:7.0
+ script:
+ - phpunit --configuration phpunit_myapp.xml
+```
+
+### Custom PHP configuration in Docker builds
+
+There are times where you will need to customise your PHP environment by
+putting your `.ini` file into `/usr/local/etc/php/conf.d/`. For that purpose
+add a `before_script` action:
+
+```yaml
+before_script:
+- cp my_php.ini /usr/local/etc/php/conf.d/test.ini
+```
+
+Of course, `my_php.ini` must be present in the root directory of your repository.
+
+## Test PHP projects using the Shell executor
+
+The shell executor runs your builds in a terminal session on your server.
+Thus, in order to test your projects you first need to make sure that all
+dependencies are installed.
+
+For example, in a VM running Debian 8 we first update the cache, then we
+install `phpunit` and `php5-mysql`:
+
+```bash
+sudo apt-get update -y
+sudo apt-get install -y phpunit php5-mysql
+```
+
+Next, add the following snippet to your `.gitlab-ci.yml`:
+
+```yaml
+test:app:
+ script:
+ - phpunit --configuration phpunit_myapp.xml
+```
+
+Finally, push to GitLab and let the tests begin!
+
+### Test against different PHP versions in Shell builds
+
+The [phpenv][] project allows you to easily manage different versions of PHP
+each with its own config. This is specially usefull when testing PHP projects
+with the Shell executor.
+
+You will have to install it on your build machine under the `gitlab-runner`
+user following [the upstream installation guide][phpenv-installation].
+
+Using phpenv also allows to easily configure the PHP environment with:
+
+```
+phpenv config-add my_config.ini
+```
+
+*__Important note:__ It seems `phpenv/phpenv`
+ [is abandoned](https://github.com/phpenv/phpenv/issues/57). There is a fork
+ at [madumlao/phpenv](https://github.com/madumlao/phpenv) that tries to bring
+ the project back to life. [CHH/phpenv](https://github.com/CHH/phpenv) also
+ seems like a good alternative. Picking any of the mentioned tools will work
+ with the basic phpenv commands. Guiding you to choose the right phpenv is out
+ of the scope of this tutorial.*
+
+### Install custom extensions
+
+Since this is a pretty bare installation of the PHP environment, you may need
+some extensions that are not currently present on the build machine.
+
+To install additional extensions simply execute:
+
+```bash
+pecl install <extension>
+```
+
+It's not advised to add this to `.gitlab-ci.yml`. You should execute this
+command once, only to setup the build environment.
+
+## Extend your tests
+
+### Using atoum
+
+Instead of PHPUnit, you can use any other tool to run unit tests. For example
+you can use [atoum](https://github.com/atoum/atoum):
+
+```yaml
+before_script:
+- wget http://downloads.atoum.org/nightly/mageekguy.atoum.phar
+
+test:atoum:
+ script:
+ - php mageekguy.atoum.phar
+```
+
+### Using Composer
+
+The majority of the PHP projects use Composer for managing their PHP packages.
+In order to execute Composer before running your tests, simply add the
+following in your `.gitlab-ci.yml`:
+
+```yaml
+...
+
+# Composer stores all downloaded packages in the vendor/ directory.
+# Do not use the following if the vendor/ directory is commited to
+# your git repository.
+cache:
+ paths:
+ - vendor/
+
+before_script:
+# Install composer dependencies
+- curl -sS https://getcomposer.org/installer | php
+- php composer.phar install
+
+...
+```
+
+## Access private packages / dependencies
+
+If your test suite needs to access a private repository, you need to configure
+[the SSH keys](../ssh_keys/README.md) in order to be able to clone it.
+
+## Use databases or other services
+
+Most of the time you will need a running database in order for your tests to
+run. If you are using the Docker executor you can leverage Docker's ability to
+link to other containers. In GitLab Runner lingo, this can be achieved by
+defining a `service`.
+
+This functionality is covered in [the CI services](../services/README.md)
+documentation.
+
+## Testing things locally
+
+With GitLab Runner 1.0 you can also test any changes locally. From your
+terminal execute:
+
+```bash
+# Check using docker executor
+gitlab-runner exec docker test:app
+
+# Check using shell executor
+gitlab-runner exec shell test:app
+```
+
+## Example project
+
+We have set up an [Example PHP Project][php-example-repo] for your convenience
+that runs on [GitLab.com](https://gitlab.com) using our publicly available
+[shared runners](../runners/README.md).
+
+Want to hack on it? Simply fork it, commit and push your changes. Within a few
+moments the changes will be picked by a public runner and the build will begin.
+
+[php-hub]: https://hub.docker.com/_/php/
+[phpenv]: https://github.com/phpenv/phpenv
+[phpenv-installation]: https://github.com/phpenv/phpenv#installation
+[php-example-repo]: https://gitlab.com/gitlab-examples/php
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index a87a1f806fc..a9b36139de9 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -1,44 +1,62 @@
# Quick Start
-To start building projects with GitLab CI a few steps needs to be done.
+Starting from version 8.0, GitLab Continuous Integration (CI) is fully
+integrated into GitLab itself and is enabled by default on all projects.
-## 1. Install GitLab and CI
+This guide assumes that you:
-First you need to have a working GitLab and GitLab CI instance.
+- have a working GitLab instance of version 8.0 or higher or are using
+ [GitLab.com](https://gitlab.com/users/sign_in)
+- have a project in GitLab that you would like to use CI for
-You can omit this step if you use [GitLab.com](http://GitLab.com/).
+In brief, the steps needed to have a working CI can be summed up to:
-## 2. Create repository on GitLab
+1. Create a new project
+1. Add `.gitlab-ci.yml` to the git repository and push to GitLab
+1. Configure a Runner
-Once you login on your GitLab add a new repository where you will store your source code.
-Push your application to that repository.
+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.
-## 3. Add project to CI
+Now, let's break it down to pieces and work on solving the GitLab CI puzzle.
-The next part is to login to GitLab CI.
-Point your browser to the URL you have set GitLab or use [gitlab.com/ci](http://gitlab.com/ci/).
+## Creating a `.gitlab-ci.yml` file
-On the first screen you will see a list of GitLab's projects that you have access to:
+Before you create `.gitlab-ci.yml` let's first explain in brief what this is
+all about.
-![Projects](projects.png)
+### What is `.gitlab-ci.yml`
-Click **Add Project to CI**.
-This will create project in CI and authorize GitLab CI to fetch sources from GitLab.
+The `.gitlab-ci.yml` file is where you configure what CI does with your project.
+It lives in the root of your repository.
-> GitLab CI creates unique token that is used to configure GitLab CI service in GitLab.
-> This token allows to access GitLab's repository and configures GitLab to trigger GitLab CI webhook on **Push events** and **Tag push events**.
-> You can see that token by going to Project's Settings > Services > GitLab CI.
-> You will see there token, the same token is assigned in GitLab CI settings of project.
+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.
-## 4. Create project's configuration - .gitlab-ci.yml
+Because `.gitlab-ci.yml` is in the repository, it is version controlled,
+old versions still build succesfully, 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].
-The next: You have to define how your project will be built.
-GitLab CI uses [YAML](https://en.wikipedia.org/wiki/YAML) file to store build configuration.
-You need to create `.gitlab-ci.yml` in root directory of your repository:
+**Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
+so you have to pay extra attention to the identation. Always use spaces, not
+tabs.
+
+### Creating a simple `.gitlab-ci.yml` file
+
+You need to create a file named `.gitlab-ci.yml` in the root directory of your
+repository. Below is an example for a Ruby on Rails project.
```yaml
before_script:
- - bundle install
+ - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs
+ - ruby -v
+ - which ruby
+ - gem install bundler --no-ri --no-rdoc
+ - bundle install --jobs $(nproc) "${FLAGS[@]}"
rspec:
script:
@@ -49,71 +67,131 @@ rubocop:
- bundle exec rubocop
```
-This is the simplest possible build configuration that will work for most Ruby applications:
-1. Define two jobs `rspec` and `rubocop` with two different commands to be executed.
-1. Before every job execute commands defined by `before_script`.
+This is the simplest possible build configuration that will work for most Ruby
+applications:
+
+1. Define two jobs `rspec` and `rubocop` (the names are arbitrary) with
+ different commands to be executed.
+1. Before every job, the commands defined by `before_script` are executed.
-The `.gitlab-ci.yml` defines set of jobs with constrains how and when they should be run.
-The jobs are defined as top-level elements with name and always have to contain the `script`.
-Jobs are used to create builds, which are then picked by [runners](../runners/README.md) and executed within environment of the runner.
-What is important that each job is run independently from each other.
+The `.gitlab-ci.yml` file defines sets of jobs with constraints of how and when
+they should be run. The jobs are defined as top-level elements with a name (in
+our case `rspec` and `rubocop`) and always have to contain the `script` keyword.
+Jobs are used to create builds, which are then picked by
+[Runners](../runners/README.md) and executed within the environment of the Runner.
-For more information and complete `.gitlab-ci.yml` syntax, please check the [Configuring project (.gitlab-ci.yml)](../yaml/README.md).
+What is important is that each job is run independently from each other.
-## 5. Add file and push .gitlab-ci.yml to repository
+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.
-Once you created `.gitlab-ci.yml` you should add it to git repository and push it to GitLab.
+For more information and a complete `.gitlab-ci.yml` syntax, please check
+[the documentation on .gitlab-ci.yml](../yaml/README.md).
+
+### Push `.gitlab-ci.yml` to GitLab
+
+Once you've created `.gitlab-ci.yml`, you should add it to your git repository
+and push it to GitLab.
```bash
git add .gitlab-ci.yml
-git commit
+git commit -m "Add .gitlab-ci.yml"
git push origin master
```
-If you refresh the project's page on GitLab CI you will notice a one new commit:
+Now if you go to the **Builds** page you will see that the builds are pending.
+
+You can also go to the **Commits** page and notice the little clock icon next
+to the commit SHA.
+
+![New commit pending](img/new_commit.png)
+
+Clicking on the clock icon you will be directed to the builds page for that
+specific commit.
+
+![Single commit builds page](img/single_commit_status_pending.png)
+
+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.
+
+## 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 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_.
+
+Find more information about different Runners in the
+[Runners](../runners/README.md) documentation.
+
+You can find whether any Runners are assigned to your project by going to
+**Settings > Runners**. Setting up a Runner is easy and straightforward. The
+official Runner supported by GitLab is written in Go and can be found at
+<https://gitlab.com/gitlab-org/gitlab-ci-multi-runner>.
+
+In order to have a functional Runner you need to follow two steps:
+
+1. [Install it][runner-install]
+2. [Configure it](../runners/README.md#registering-a-specific-runner)
+
+Follow the links above to set up your own Runner or use a Shared Runner as
+described in the next section.
+
+For other types of unofficial Runners written in other languages, see the
+[instructions for the various GitLab Runners](https://about.gitlab.com/gitlab-ci/#gitlab-runner).
+
+Once the Runner has been set up, you should see it on the Runners page of your
+project, following **Settings > Runners**.
-![](new_commit.png)
+![Activated runners](img/runners_activated.png)
-However the commit has status **pending** which means that commit was not yet picked by runner.
+### Shared Runners
-## 6. Configure runner
+If you use [GitLab.com](https://gitlab.com/) you can use **Shared Runners**
+provided by GitLab Inc.
-In GitLab CI, Runners run your builds.
-A runner is a machine (can be virtual, bare-metal or VPS) that picks up builds through the coordinator API of GitLab CI.
+These are special virtual machines that run on GitLab's infrastructure and can
+build any project.
-A runner can be specific to a certain project or serve any project in GitLab CI.
-A runner that serves all projects is called a shared runner.
-More information about different runner types can be found in [Configuring runner](../runners/README.md).
+To enable **Shared Runners** you have to go to your project's
+**Settings > Runners** and click **Enable shared runners**.
-To check if you have runners assigned to your project go to **Runners**. You will find there information how to setup project specific runner:
+[Read more on Shared Runners](../runners/README.md).
-1. Install GitLab Runner software. Checkout the [GitLab Runner](https://about.gitlab.com/gitlab-ci/#gitlab-runner) section to install it.
-1. Specify following URL during runner setup: https://gitlab.com/ci/
-1. Use the following registration token during setup: TOKEN
+## Seeing the status of your build
-If you do it correctly your runner should be shown under **Runners activated for this project**:
+After configuring the Runner succesfully, you should see the status of your
+last commit change from _pending_ to either _running_, _success_ or _failed_.
-![](runners_activated.png)
+You can view all builds, by going to the **Builds** page in your project.
-### Shared runners
+![Commit status](img/builds_status.png)
-If you use [gitlab.com/ci](http://gitlab.com/ci/) you can use **Shared runners** provided by GitLab Inc.
-These are special virtual machines that are run on GitLab's infrastructure that can build any project.
-To enable **Shared runners** you have to go to **Runners** and click **Enable shared runners** for this project.
+By clicking on a Build ID, you will be able to see the log of that build.
+This is important to diagnose why a build failed or acted differently than
+you expected.
-## 7. Check status of commit
+![Build log](img/build_log.png)
-If everything went OK and you go to commit, the status of the commit should change from **pending** to either **running**, **success** or **failed**.
+You are also able to view the status of any commit in the various pages in
+GitLab, such as **Commits** and **Merge Requests**.
-![](commit_status.png)
+## Next steps
-You can click **Build ID** to view build log for specific job.
+Awesome! You started using CI in GitLab!
-## 8. Congratulations!
+Next you can look into doing more with the CI. Many people are using GitLab
+to package, containerize, test and deploy software.
-You managed to build your first project using GitLab CI.
-You may need to tune your `.gitlab-ci.yml` file to implement build plan for your project.
-A few examples how it can be done you can find on [Examples](../examples/README.md) page.
+Visit our various languages examples at <https://gitlab.com/groups/gitlab-examples>.
-GitLab CI also offers **the Lint** tool to verify validity of your `.gitlab-ci.yml` which can be useful to troubleshoot potential problems.
-The Lint is available from project's settings or by adding `/lint` to GitLab CI url.
+[runner-install]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/tree/master#installation
+[blog-ci]: https://about.gitlab.com/2015/05/06/why-were-replacing-gitlab-ci-jobs-with-gitlab-ci-dot-yml/
diff --git a/doc/ci/quick_start/build_status.png b/doc/ci/quick_start/build_status.png
deleted file mode 100644
index 333259e6acd..00000000000
--- a/doc/ci/quick_start/build_status.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png
deleted file mode 100644
index 725b79e6f91..00000000000
--- a/doc/ci/quick_start/commit_status.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/img/build_log.png b/doc/ci/quick_start/img/build_log.png
new file mode 100644
index 00000000000..89e6cd40cb6
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..b8e6c2a361a
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..3d3c9d5c0bd
--- /dev/null
+++ b/doc/ci/quick_start/img/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/img/runners_activated.png b/doc/ci/quick_start/img/runners_activated.png
new file mode 100644
index 00000000000..eafcfd6ecd5
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..23b3bb5acfc
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..a049ec2a5ba
--- /dev/null
+++ b/doc/ci/quick_start/img/status_pending.png
Binary files differ
diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png
deleted file mode 100644
index 3839e893c17..00000000000
--- a/doc/ci/quick_start/new_commit.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/projects.png
deleted file mode 100644
index 0b3430a69db..00000000000
--- a/doc/ci/quick_start/projects.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/runners.png
deleted file mode 100644
index 25b4046bc00..00000000000
--- a/doc/ci/quick_start/runners.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png
deleted file mode 100644
index c934bd12f41..00000000000
--- a/doc/ci/quick_start/runners_activated.png
+++ /dev/null
Binary files differ
diff --git a/doc/ci/services/README.md b/doc/ci/services/README.md
new file mode 100644
index 00000000000..1ebb0a4a250
--- /dev/null
+++ b/doc/ci/services/README.md
@@ -0,0 +1,9 @@
+## GitLab CI Services
+
+GitLab CI uses the `services` keyword to define what docker containers should be
+linked with your base image. Below is a list of examples you may use.
+
++ [Using MySQL](mysql.md)
++ [Using PostgreSQL](postgres.md)
++ [Using Redis](redis.md)
++ [Using Other Services](../docker/using_docker_images.md#how-to-use-other-images-as-services)
diff --git a/doc/ci/services/docker-services.md b/doc/ci/services/docker-services.md
new file mode 100644
index 00000000000..df36ebaf7d4
--- /dev/null
+++ b/doc/ci/services/docker-services.md
@@ -0,0 +1,5 @@
+## GitLab CI Services
+
++ [Using MySQL](mysql.md)
++ [Using PostgreSQL](postgres.md)
++ [Using Redis](redis.md)
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
new file mode 100644
index 00000000000..c66d77122b2
--- /dev/null
+++ b/doc/ci/services/mysql.md
@@ -0,0 +1,118 @@
+# Using MySQL
+
+As many applications depend on MySQL as their database, you will eventually
+need it in order for your tests to run. Below you are guided how to do this
+with the Docker and Shell executors of GitLab Runner.
+
+## Use MySQL with the Docker executor
+
+If you are using [GitLab Runner](../runners/README.md) with the Docker executor
+you basically have everything set up already.
+
+First, in your `.gitlab-ci.yml` add:
+
+```yaml
+services:
+ - mysql:latest
+
+variables:
+ # Configure mysql environment variables (https://hub.docker.com/_/mysql/)
+ MYSQL_DATABASE: el_duderino
+ MYSQL_ROOT_PASSWORD: mysql_strong_password
+```
+
+And then configure your application to use the database, for example:
+
+```yaml
+Host: mysql
+User: root
+Password: mysql_strong_password
+Database: el_duderino
+```
+
+If you are wondering why we used `mysql` for the `Host`, read more at
+[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build).
+
+You can also use any other docker image available on [Docker Hub][hub-mysql].
+For example, to use MySQL 5.5 the service becomes `mysql:5.5`.
+
+The `mysql` image can accept some environment variables. For more details
+check the documentation on [Docker Hub][hub-mysql].
+
+## Use MySQL with the Shell executor
+
+You can also use MySQL on manually configured servers that are using
+GitLab Runner with the Shell executor.
+
+First install the MySQL server:
+
+```bash
+sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev
+```
+
+Pick a MySQL root password (can be anything), and type it twice when asked.
+
+*Note: As a security measure you can run `mysql_secure_installation` to
+remove anonymous users, drop the test database and disable remote logins with
+the root user.*
+
+The next step is to create a user, so login to MySQL as root:
+
+```bash
+mysql -u root -p
+```
+
+Then create a user (in our case `runner`) which will be used by your
+application. Change `$password` in the command below to a real strong password.
+
+*Note: Do not type `mysql>`, this is part of the MySQL prompt.*
+
+```bash
+mysql> CREATE USER 'runner'@'localhost' IDENTIFIED BY '$password';
+```
+
+Create the database:
+
+```bash
+mysql> CREATE DATABASE IF NOT EXISTS `el_duderino` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
+```
+
+Grant the necessary permissions on the database:
+
+```bash
+mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `el_duderino`.* TO 'runner'@'localhost';
+```
+
+If all went well you can now quit the database session:
+
+```bash
+mysql> \q
+```
+
+Now, try to connect to the newly created database to check that everything is
+in place:
+
+```bash
+mysql -u runner -p -D el_duderino
+```
+
+As a final step, configure your application to use the database, for example:
+
+```bash
+Host: localhost
+User: runner
+Password: $password
+Database: el_duderino
+```
+
+## Example project
+
+We have set up an [Example MySQL Project][mysql-example-repo] for your
+convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
+available [shared runners](../runners/README.md).
+
+Want to hack on it? Simply fork it, commit and push your changes. Within a few
+moments the changes will be picked by a public runner and the build will begin.
+
+[hub-mysql]: https://hub.docker.com/_/mysql/
+[mysql-example-repo]: https://gitlab.com/gitlab-examples/mysql
diff --git a/doc/ci/services/postgres.md b/doc/ci/services/postgres.md
new file mode 100644
index 00000000000..17d21dbda1c
--- /dev/null
+++ b/doc/ci/services/postgres.md
@@ -0,0 +1,114 @@
+# Using PostgreSQL
+
+As many applications depend on PostgreSQL as their database, you will
+eventually need it in order for your tests to run. Below you are guided how to
+do this with the Docker and Shell executors of GitLab Runner.
+
+## Use PostgreSQL with the Docker executor
+
+If you are using [GitLab Runner](../runners/README.md) with the Docker executor
+you basically have everything set up already.
+
+First, in your `.gitlab-ci.yml` add:
+
+```yaml
+services:
+ - postgres:latest
+
+variables:
+ POSTGRES_DB: nice_marmot
+ POSTGRES_USER: runner
+ POSTGRES_PASSWORD: ""
+```
+
+And then configure your application to use the database, for example:
+
+```yaml
+Host: postgres
+User: runner
+Password:
+Database: nice_marmot
+```
+
+If you are wondering why we used `postgres` for the `Host`, read more at
+[How is service linked to the build](../docker/using_docker_images.md#how-is-service-linked-to-the-build).
+
+You can also use any other docker image available on [Docker Hub][hub-pg].
+For example, to use PostgreSQL 9.3 the service becomes `postgres:9.3`.
+
+The `postgres` image can accept some environment variables. For more details
+check the documentation on [Docker Hub][hub-pg].
+
+## Use PostgreSQL with the Shell executor
+
+You can also use PostgreSQL on manually configured servers that are using
+GitLab Runner with the Shell executor.
+
+First install the PostgreSQL server:
+
+```bash
+sudo apt-get install -y postgresql postgresql-client libpq-dev
+```
+
+The next step is to create a user, so login to PostgreSQL:
+
+```bash
+sudo -u postgres psql -d template1
+```
+
+Then create a user (in our case `runner`) which will be used by your
+application. Change `$password` in the command below to a real strong password.
+
+*__Note:__ Do not type `template1=#`, this is part of the PostgreSQL prompt.*
+
+```bash
+template1=# CREATE USER runner WITH PASSWORD '$password' CREATEDB;
+```
+
+*__Note:__ Notice that we created the user with the privilege to be able to
+create databases (`CREATEDB`). In the following steps we will create a database
+explicitly for that user but having that privilege can be useful if in your
+testing framework you have tools that drop and create databases.*
+
+Create the database and grant all privileges on it for the user `runner`:
+
+```bash
+template1=# CREATE DATABASE nice_marmot OWNER runner;
+```
+
+If all went well you can now quit the database session:
+
+```bash
+template1=# \q
+```
+
+Now, try to connect to the newly created database with the user `runner` to
+check that everything is in place.
+
+```bash
+psql -U runner -h localhost -d nice_marmot -W
+```
+
+*__Note:__ We are explicitly telling `psql` to connect to localhost in order
+to use the md5 authentication. If you omit this step you will be denied access.*
+
+Finally, configure your application to use the database, for example:
+
+```yaml
+Host: localhost
+User: runner
+Password: $password
+Database: nice_marmot
+```
+
+## Example project
+
+We have set up an [Example PostgreSQL Project][postgres-example-repo] for your
+convenience that runs on [GitLab.com](https://gitlab.com) using our publicly
+available [shared runners](../runners/README.md).
+
+Want to hack on it? Simply fork it, commit and push your changes. Within a few
+moments the changes will be picked by a public runner and the build will begin.
+
+[hub-pg]: https://hub.docker.com/_/postgres/
+[postgres-example-repo]: https://gitlab.com/gitlab-examples/postgres
diff --git a/doc/ci/services/redis.md b/doc/ci/services/redis.md
new file mode 100644
index 00000000000..b281e8f9f60
--- /dev/null
+++ b/doc/ci/services/redis.md
@@ -0,0 +1,69 @@
+# Using Redis
+
+As many applications depend on Redis as their key-value store, you will
+eventually need it in order for your tests to run. Below you are guided how to
+do this with the Docker and Shell executors of GitLab Runner.
+
+## Use Redis with the Docker executor
+
+If you are using [GitLab Runner](../runners/README.md) with the Docker executor
+you basically have everything set up already.
+
+First, in your `.gitlab-ci.yml` add:
+
+```yaml
+services:
+ - redis:latest
+```
+
+Then you need to configure your application to use the Redis database, for
+example:
+
+```yaml
+Host: redis
+```
+
+And that's it. Redis will now be available to be used within your testing
+framework.
+
+You can also use any other docker image available on [Docker Hub][hub-redis].
+For example, to use Redis 2.8 the service becomes `redis:2.8`.
+
+## Use Redis with the Shell executor
+
+Redis can also be used on manually configured servers that are using GitLab
+Runner with the Shell executor.
+
+In your build machine install the Redis server:
+
+```bash
+sudo apt-get install redis-server
+```
+
+Verify that you can connect to the server with the `gitlab-runner` user:
+
+```bash
+# Try connecting the the Redis server
+sudo -u gitlab-runner -H redis-cli
+
+# Quit the session
+127.0.0.1:6379> quit
+```
+
+Finally, configure your application to use the database, for example:
+
+```yaml
+Host: localhost
+```
+
+## Example project
+
+We have set up an [Example Redis Project][redis-example-repo] for your convenience
+that runs on [GitLab.com](https://gitlab.com) using our publicly available
+[shared runners](../runners/README.md).
+
+Want to hack on it? Simply fork it, commit and push your changes. Within a few
+moments the changes will be picked by a public runner and the build will begin.
+
+[hub-redis]: https://hub.docker.com/_/redis/
+[redis-example-repo]: https://gitlab.com/gitlab-examples/redis
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
new file mode 100644
index 00000000000..210f9c3e849
--- /dev/null
+++ b/doc/ci/ssh_keys/README.md
@@ -0,0 +1,109 @@
+# Using SSH keys
+
+GitLab currently doesn't have built-in support for managing SSH keys in a build
+environment.
+
+The SSH keys can be useful when:
+
+1. You want to checkout internal submodules
+2. You want to download private packages using your package manager (eg. bundler)
+3. You want to deploy your application to eg. Heroku or your own server
+4. You want to execute SSH commands from the build server to the remote server
+5. You want to rsync files from your build server to the remote server
+
+If anything of the above rings a bell, then you most likely need an SSH key.
+
+## Inject keys in your build server
+
+The most widely supported method is to inject an SSH key into your build
+environment by extending your `.gitlab-ci.yml`.
+
+This is the universal solution which works with any type of executor
+(docker, shell, etc.).
+
+### How it works
+
+1. Create a new SSH key pair with [ssh-keygen][]
+2. Add the private key as a **Secret Variable** to the project
+3. Run the [ssh-agent][] during build to load the private key.
+
+## SSH keys when using the Docker executor
+
+You will first need to create an SSH key pair. For more information, follow the
+instructions to [generate an SSH key](../ssh/README.md).
+
+Then, create a new **Secret Variable** in your project settings on GitLab
+following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
+and in the **Value** field paste the content of your _private_ key that you
+created earlier.
+
+Next you need to modify your `.gitlab-ci.yml` with a `before_script` action.
+Add it to the top:
+
+```
+before_script:
+ # Install ssh-agent if not already installed, it is required by Docker.
+ # (change apt-get to yum if you use a CentOS-based image)
+ - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
+
+ # Run ssh-agent (inside the build environment)
+ - eval $(ssh-agent -s)
+
+ # Add the SSH key stored in SSH_PRIVATE_KEY variable to the agent store
+ - ssh-add <(echo "$SSH_PRIVATE_KEY")
+
+ # For Docker builds disable host key checking. Be aware that by adding that
+ # you are suspectible to man-in-the-middle attacks.
+ # WARNING: Use this only with the Docker executor, if you use it with shell
+ # you will overwrite your user's SSH config.
+ - mkdir -p ~/.ssh
+ - '[[ -f /.dockerinit ]] && echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config`
+```
+
+As a final step, add the _public_ key from the one you created earlier to the
+services that you want to have an access to from within the build environment.
+If you are accessing a private GitLab repository you need to add it as a
+[deploy key](../ssh/README.md#deploy-keys).
+
+That's it! You can now have access to private servers or repositories in your
+build environment.
+
+## SSH keys when using the Shell executor
+
+If you are using the Shell executor and not Docker, it is easier to set up an
+SSH key.
+
+You can generate the SSH key from the machine that GitLab Runner is installed
+on, and use that key for all projects that are run on this machine.
+
+First, you need to login to the server that runs your builds.
+
+Then from the terminal login as the `gitlab-runner` user and generate the SSH
+key pair as described in the [SSH keys documentation](../ssh/README.md).
+
+As a final step, add the _public_ key from the one you created earlier to the
+services that you want to have an access to from within the build environment.
+If you are accessing a private GitLab repository you need to add it as a
+[deploy key](../ssh/README.md#deploy-keys).
+
+Once done, try to login to the remote server in order to accept the fingerprint:
+
+```bash
+ssh <address-of-my-server>
+```
+
+For accessing repositories on GitLab.com, the `<address-of-my-server>` would be
+`git@gitlab.com`.
+
+## Example project
+
+We have set up an [Example SSH Project][ssh-example-repo] for your convenience
+that runs on [GitLab.com](https://gitlab.com) using our publicly available
+[shared runners](../runners/README.md).
+
+Want to hack on it? Simply fork it, commit and push your changes. Within a few
+moments the changes will be picked by a public runner and the build will begin.
+
+[ssh-keygen]: http://linux.die.net/man/1/ssh-keygen
+[ssh-agent]: http://linux.die.net/man/1/ssh-agent
+[ssh-example-repo]: https://gitlab.com/gitlab-examples/ssh-private-key/
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
new file mode 100644
index 00000000000..9f7c1bfe6a0
--- /dev/null
+++ b/doc/ci/triggers/README.md
@@ -0,0 +1,172 @@
+# Triggering Builds through the API
+
+_**Note:** This feature was [introduced][ci-229] in GitLab CE 7.14_
+
+Triggers can be used to force a rebuild of a specific branch, tag or commit,
+with an API call.
+
+## Add a trigger
+
+You can add a new trigger by going to your project's **Settings > Triggers**.
+The **Add trigger** button will create a new token which you can then use to
+trigger a rebuild of this particular project.
+
+Every new trigger you create, gets assigned a different token which you can
+then use inside your scripts or `.gitlab-ci.yml`. You also have a nice
+overview of the time the triggers were last used.
+
+![Triggers page overview](img/triggers_page.png)
+
+## Revoke a trigger
+
+You can revoke a trigger any time by going at your project's
+**Settings > Triggers** and hitting the **Revoke** button. The action is
+irreversible.
+
+## Trigger a build
+
+To trigger a build you need to send a `POST` request to GitLab's API endpoint:
+
+```
+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)
+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
+the **Builds** page and the builds are marked as `triggered`.
+
+![Marked rebuilds as triggered on builds page](img/builds_page.png)
+
+---
+
+You can see which trigger caused the rebuild by visiting the single build page.
+The token of the trigger is exposed in the UI as you can see from the image
+below.
+
+![Marked rebuilds as triggered on a single build page](img/trigger_single_build.png)
+
+---
+
+See the [Examples](#examples) section for more details on how to actually
+trigger a rebuild.
+
+## Pass build variables to a trigger
+
+You can pass any number of arbitrary variables in the trigger API call and they
+will be available in GitLab CI so that they can be used in your `.gitlab-ci.yml`
+file. The parameter is of the form:
+
+```
+variables[key]=value
+```
+
+This information is also exposed in the UI.
+
+![Build variables in UI](img/trigger_variables.png)
+
+---
+
+See the [Examples](#examples) section below for more details.
+
+## Examples
+
+Using cURL you can trigger a rebuild with minimal effort, for example:
+
+```bash
+curl -X POST \
+ -F token=TOKEN \
+ -F ref=master \
+ https://gitlab.example.com/api/v3/projects/9/trigger/builds
+```
+
+In this case, the project with ID `9` will get rebuilt on `master` branch.
+
+
+### Triggering a build within `.gitlab-ci.yml`
+
+You can also benefit by using triggers in your `.gitlab-ci.yml`. Let's say that
+you have two projects, A and B, and you want to trigger a rebuild on the `master`
+branch of project B whenever a tag on project A is created. This is the job you
+need to add in project's A `.gitlab-ci.yml`:
+
+```yaml
+build_docs:
+ stage: deploy
+ script:
+ - "curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds"
+ only:
+ - tags
+```
+
+Now, whenever a new tag is pushed on project A, the build will run and the
+`build_docs` job will be executed, triggering a rebuild of project B. The
+`stage: deploy` ensures that this job will run only after all jobs with
+`stage: test` complete successfully.
+
+_**Note:** If your project is public, passing the token in plain text is
+probably not the wisest idea, so you might want to use a
+[secure variable](../variables/README.md#user-defined-variables-secure-variables)
+for that purpose._
+
+### Making use of trigger variables
+
+Using trigger variables can be proven useful for a variety of reasons.
+
+* Identifiable jobs. Since the variable is exposed in the UI you can know
+ why the rebuild was triggered if you pass a variable that explains the
+ purpose.
+* Conditional job processing. You can have conditional jobs that run whenever
+ a certain variable is present.
+
+Consider the following `.gitlab-ci.yml` where we set three
+[stages](../yaml/README.md#stages) and the `upload_package` job is run only
+when all jobs from the test and build stages pass. When the `UPLOAD_TO_S3`
+variable is non-zero, `make upload` is run.
+
+```yaml
+stages:
+- test
+- build
+- package
+
+run_tests:
+ script:
+ - make test
+
+build_package:
+ stage: build
+ script:
+ - make build
+
+upload_package:
+ stage: package
+ script:
+ - if [ -n "${UPLOAD_TO_S3}" ]; then make upload; fi
+```
+
+You can then trigger a rebuild while you pass the `UPLOAD_TO_S3` variable
+and the script of the `upload_package` job will run:
+
+```bash
+curl -X POST \
+ -F token=TOKEN \
+ -F ref=master \
+ -F "variables[UPLOAD_TO_S3]=true" \
+ https://gitlab.example.com/api/v3/projects/9/trigger/builds
+```
+
+### Using cron to trigger nightly builds
+
+Whether you craft a script or just run cURL directly, you can trigger builds
+in conjunction with cron. The example below triggers a build on the `master`
+branch of project with ID `9` every night at `00:30`:
+
+```bash
+30 0 * * * curl -X POST -F token=TOKEN -F ref=master https://gitlab.example.com/api/v3/projects/9/trigger/builds
+```
+
+[ci-229]: https://gitlab.com/gitlab-org/gitlab-ci/merge_requests/229
diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
new file mode 100644
index 00000000000..e78794fbee7
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..c25f27409d6
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..2207e8b34cb
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..268368dc3c5
--- /dev/null
+++ 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 022afb70042..b99ea25a3fe 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -27,7 +27,6 @@ The API_TOKEN will take the Secure Variable value: `SECURE`.
| **CI_BUILD_TAG** | 0.5 | The commit tag name. Present only when building tags. |
| **CI_BUILD_NAME** | 0.5 | The name of the build as defined in `.gitlab-ci.yml` |
| **CI_BUILD_STAGE** | 0.5 | The name of the stage as defined in `.gitlab-ci.yml` |
-| **CI_BUILD_BEFORE_SHA** | all | The first commit that were included in push request |
| **CI_BUILD_REF_NAME** | all | The branch or tag name for which project is built |
| **CI_BUILD_ID** | all | The unique id of the current build that GitLab CI uses internally |
| **CI_BUILD_REPO** | all | The URL to clone the Git repository |
@@ -40,7 +39,6 @@ The API_TOKEN will take the Secure Variable value: `SECURE`.
Example values:
```bash
-export CI_BUILD_BEFORE_SHA="9df57456fa9de2a6d335ca5edf9750ed812b9df0"
export CI_BUILD_ID="50"
export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
export CI_BUILD_REF_NAME="master"
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index ea8f72bc135..fd0d49de4e4 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1,9 +1,12 @@
# Configuration of your builds with .gitlab-ci.yml
-From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (**.gitlab-ci.yml**) for the project configuration.
-It is placed in the root 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:
+From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML)
+file (`.gitlab-ci.yml`) for the project configuration. It is placed in the root
+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:
```yaml
job1:
@@ -13,15 +16,21 @@ job2:
script: "execute-script-for-job2"
```
-The above example is the simplest possible CI configuration with two separate jobs,
-where each of the jobs executes a different command.
-Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository.
+The above example is the simplest possible CI configuration with two separate
+jobs, where each of the jobs executes a different command.
+
+Of course a command can execute code directly (`./configure;make;make install`)
+or run a script (`test.sh`) in the repository.
-Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner.
-What is important, is that each job is run independently from each other.
+Jobs are used to create builds, which are then picked up by
+[runners](../runners/README.md) and executed within the environment of the
+runner. What is important, is that each job is run independently from each
+other.
## .gitlab-ci.yml
-The YAML syntax allows for using more complex job specifications than in the above example:
+
+The YAML syntax allows for using more complex job specifications than in the
+above example:
```yaml
image: ruby:2.1
@@ -46,25 +55,31 @@ job1:
- docker
```
-There are a few `keywords` that can't be used as job names:
+There are a few reserved `keywords` that **cannot** be used as job names:
-| keyword | required | description |
+| Keyword | Required | Description |
|---------------|----------|-------------|
-| image | optional | Use docker image, covered in [Use Docker](../docker/README.md) |
-| services | optional | Use docker services, covered in [Use Docker](../docker/README.md) |
-| stages | optional | Define build stages |
-| types | optional | Alias for `stages` |
-| before_script | optional | Define commands prepended for each job's script |
-| variables | optional | Define build variables |
+| image | no | Use docker image, covered in [Use Docker](../docker/README.md) |
+| services | no | Use docker services, covered in [Use Docker](../docker/README.md) |
+| stages | no | Define build stages |
+| types | no | Alias for `stages` |
+| before_script | no | Define commands that run before each job's script |
+| variables | no | Define build variables |
+| cache | no | Define list of files that should be cached between subsequent runs |
### image and services
-This allows to specify a custom Docker image and a list of services that can be used for time of the build.
-The configuration of this feature is covered in separate document: [Use Docker](../docker/README.md).
+
+This allows to specify a custom Docker image and a list of services that can be
+used for time of the build. The configuration of this feature is covered in
+separate document: [Use Docker](../docker/README.md).
### before_script
-`before_script` is used to define the command that should be run before all builds, including deploy builds. This can be an array or a multiline string.
+
+`before_script` is used to define the command that should be run before all
+builds, including deploy builds. This can be an array or a multi-line string.
### stages
+
`stages` is used to define build stages that can be used by jobs.
The specification of `stages` allows for having flexible multi stage pipelines.
@@ -74,7 +89,8 @@ The ordering of elements in `stages` defines the ordering of builds' execution:
1. Builds of next stage are run after success.
Let's consider the following example, which defines 3 stages:
-```
+
+```yaml
stages:
- build
- test
@@ -85,21 +101,26 @@ stages:
1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel.
1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel.
1. If all jobs of `deploy` succeeds, the commit is marked as `success`.
-1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed.
+1. If any of the previous jobs fails, the commit is marked as `failed` and no
+ jobs of further stage are executed.
There are also two edge cases worth mentioning:
-1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default.
+1. If no `stages` is 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.
### types
+
Alias for [stages](#stages).
### variables
-**This feature requires `gitlab-runner` with version equal or greater than 0.5.0.**
-GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
-The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
+_**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:
```yaml
variables:
@@ -108,12 +129,34 @@ variables:
These variables can be later used in all executed commands and scripts.
-The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+The YAML-defined variables are also set to all created service containers,
+thus allowing to fine tune them.
+
+### cache
+
+`cache` is used to specify a list of files and directories which should be
+cached between builds. Caches are stored according to the branch/ref and the
+job name. They are not currently shared between different job names or between
+branches/refs, which means that caching will benefit you if you push subsequent
+commits to an existing feature branch.
+
+If `cache` is defined outside the scope of the jobs, it means it is set
+globally and all jobs will use its definition.
+
+To cache all git untracked files and files in `binaries`:
+
+```yaml
+cache:
+ untracked: true
+ paths:
+ - binaries/
+```
## Jobs
-`.gitlab-ci.yml` allows you to specify an unlimited number of jobs.
-Each job has to have a unique `job_name`, which is not one of the keywords mentioned above.
-A job is defined by a list of parameters that define the build behaviour.
+
+`.gitlab-ci.yml` allows you to specify an unlimited number of jobs. Each job
+must have a unique name, which is not one of the Keywords mentioned above.
+A job is defined by a list of parameters that define the build behavior.
```yaml
job_name:
@@ -131,19 +174,22 @@ job_name:
allow_failure: true
```
-| keyword | required | description |
+| Keyword | Required | Description |
|---------------|----------|-------------|
-| script | required | Defines a shell script which is executed by runner |
-| stage | optional (default: test) | Defines a build stage |
-| type | optional | Alias for `stage` |
-| only | optional | Defines a list of git refs for which build is created |
-| except | optional | Defines a list of git refs for which build is not created |
-| tags | optional | Defines a list of tags which are used to select runner |
-| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status |
-| when | optional | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| script | yes | Defines a shell script which is executed by runner |
+| stage | no (default: `test`) | Defines a build stage |
+| type | no | Alias for `stage` |
+| only | no | Defines a list of git refs for which build is created |
+| except | no | Defines a list of git refs for which build is not created |
+| tags | no | Defines a list of tags which are used to select runner |
+| allow_failure | no | Allow build to fail. Failed build doesn't contribute to commit status |
+| when | no | Define when to run build. Can be `on_success`, `on_failure` or `always` |
+| artifacts | no | Define list build artifacts |
+| cache | no | Define list of files that should be cached between subsequent runs |
### script
-`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`.
+
+`script` is a shell script which is executed by the runner. For example:
```yaml
job:
@@ -151,6 +197,7 @@ job:
```
This parameter can also contain several commands using an array:
+
```yaml
job:
script:
@@ -159,54 +206,94 @@ job:
```
### stage
-`stage` allows to group build into different stages. Builds of the same `stage` are executed in `parallel`.
-For more info about the use of `stage` please check the [stages](#stages).
+
+`stage` allows to group build into different stages. Builds of the same `stage`
+are executed in `parallel`. For more info about the use of `stage` please check
+[stages](#stages).
### only and except
-This are two parameters that allow for setting a refs policy to limit when jobs are built:
-1. `only` defines the names of branches and tags for which job will be built.
-2. `except` defines the names of branches and tags for which the job wil **not** be built.
-There are a few rules that apply to usage of refs policy:
+`only` and `except` are two parameters that set a refs policy to limit when
+jobs are built:
+
+1. `only` defines the names of branches and tags for which the job will be
+ built.
+2. `except` defines the names of branches and tags for which the job will
+ **not** be built.
+
+There are a few rules that apply to the usage of refs policy:
+
+* `only` and `except` are inclusive. If both `only` and `except` are defined
+ in a job specification, the ref is filtered by `only` and `except`.
+* `only` and `except` allow the use of regular expressions.
+* `only` and `except` allow the use of special keywords: `branches` and `tags`.
+* `only` and `except` allow to specify a repository path to filter jobs for
+ forks.
+
+In the example below, `job` will run only for refs that start with `issue-`,
+whereas all branches will be skipped.
+
+```yaml
+job:
+ # use regexp
+ only:
+ - /^issue-.*$/
+ # use special keyword
+ except:
+ - branches
+```
-1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account.
-1. `only` and `except` allow for using the regexp expressions.
-1. `only` and `except` allow for using special keywords: `branches` and `tags`.
-These names can be used for example to exclude all tags and all branches.
+The repository path can be used to have jobs executed only for the parent
+repository and not forks:
```yaml
job:
only:
- - /^issue-.*$/ # use regexp
+ - branches@gitlab-org/gitlab-ce
except:
- - branches # use special keyword
+ - master@gitlab-org/gitlab-ce
```
+The above example will run `job` for all branches on `gitlab-org/gitlab-ce`,
+except master.
+
### tags
-`tags` is used to select specific runners from the list of all runners that are allowed to run this project.
-During registration of a runner, you can specify the runner's tags, ie.: `ruby`, `postgres`, `development`.
-`tags` allow you to run builds with runners that have the specified tags assigned:
+`tags` is used to select specific runners from the list of all runners that are
+allowed to run this project.
-```
+During the registration of a runner, you can specify the runner's tags, for
+example `ruby`, `postgres`, `development`.
+
+`tags` allow you to run builds with runners that have the specified tags
+assigned to them:
+
+```yaml
job:
tags:
- ruby
- postgres
```
-The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined.
+The specification above, will make sure that `job` is built by a runner that
+has both `ruby` AND `postgres` tags defined.
### when
-`when` is used to implement jobs that are run in case of failure or despite the failure.
+
+`when` is used to implement jobs that are run in case of failure or despite the
+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.
-1. `on_failure` - execute build only when at least one build from prior stages failed.
+1. `on_success` - execute build only when all builds from prior stages
+ succeeded. 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.
-```
+For example:
+
+```yaml
stages:
- build
- cleanup_build
@@ -214,28 +301,28 @@ stages:
- deploy
- cleanup
-build:
+build_job:
stage: build
script:
- make build
-cleanup_build:
+cleanup_build_job:
stage: cleanup_build
script:
- cleanup build when failed
when: on_failure
-test:
+test_job:
stage: test
script:
- make test
-deploy:
+deploy_job:
stage: deploy
script:
- make deploy
-cleanup:
+cleanup_job:
stage: cleanup
script:
- cleanup after builds
@@ -243,12 +330,108 @@ cleanup:
```
The above script will:
-1. Execute `cleanup_build` only when the `build` failed,
-2. Always execute `cleanup` as the last step in pipeline.
+
+1. Execute `cleanup_build_job` only when `build_job` fails
+2. Always execute `cleanup_job` as the last step in pipeline.
+
+### artifacts
+
+_**Note:** Introduced in GitLab Runner v0.7.0. Also, the Windows shell executor
+ does not currently support artifact uploads._
+
+`artifacts` is used to specify list of files and directories which should be
+attached to build after success. Below are some examples.
+
+Send all files in `binaries` and `.config`:
+
+```yaml
+artifacts:
+ paths:
+ - binaries/
+ - .config
+```
+
+Send all git untracked files:
+
+```yaml
+artifacts:
+ untracked: true
+```
+
+Send all git untracked files and files in `binaries`:
+
+```yaml
+artifacts:
+ untracked: true
+ paths:
+ - binaries/
+```
+
+The artifacts will be send after a successful build success to GitLab, and will
+be accessible in the GitLab UI to download.
+
+### cache
+
+_**Note:** Introduced in GitLab Runner v0.7.0._
+
+`cache` is used to specify list of files and directories which should be cached
+between builds. Below are some examples:
+
+Cache all files in `binaries` and `.config`:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+ - .config
+```
+
+Cache all git untracked files:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ untracked: true
+```
+
+Cache all git untracked files and files in `binaries`:
+
+```yaml
+rspec:
+ script: test
+ cache:
+ untracked: true
+ paths:
+ - binaries/
+```
+
+Locally defined cache overwrites globally defined options. This will cache only
+`binaries/`:
+
+```yaml
+cache:
+ paths:
+ - my/files
+
+rspec:
+ script: test
+ cache:
+ paths:
+ - binaries/
+```
+
+The cache is provided on best effort basis, so don't expect that cache will be
+always present. For implementation details please check GitLab Runner.
## Validate the .gitlab-ci.yml
+
Each instance of GitLab CI has an embedded debug tool called Lint.
-You can find the link to the Lint in the project's settings page or use short url `/lint`.
+You can find the link under `/ci/lint` of your gitlab instance.
## Skipping builds
-There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped
+
+If your commit message contains `[ci skip]`, the commit will be created but the
+builds will be skipped.
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index 64f128f5a63..00edfc97ed9 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,38 +1,39 @@
# Issue closing pattern
-Here's how to close multiple issues in one commit message:
+When a commit or merge request resolves one or more issues, it is possible to automatically have these issues closed when the commit or merge request lands in the project's default branch.
-If a commit message matches the regular expression below, all issues referenced from
-the matched text will be closed. This happens when the commit is pushed or merged
-into the default branch of a project.
+If a commit message or merge request description contains a sentence matching the regular expression below, all issues referenced from
+the matched text will be closed. This happens when the commit is pushed to a project's default branch, or when a commit or merge request is merged into there.
-When not specified, the default issue_closing_pattern as shown below will be used:
+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? +)?#\d+(?:(?:, *| +and +)?))+)
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)
```
+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`).
+
For example:
```
-git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes #22). This commit is also related to #17 and fixes #18, #19 and #23."
+git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes group/otherproject#2). This commit is also related to #17 and fixes #18, #19 and https://gitlab.example.com/group/otherproject/issues/23."
```
-will close `#20`, `#21`, `#22`, `#18`, `#19` and `#23`, but `#17` won't be closed
-as it does not match the pattern. It also works with multiline commit messages.
+will close `#18`, `#19`, `#20`, and `#21` in the project this commit is pushed to, as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it does not match the pattern. It also works with multiline commit messages.
Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site
to test your own patterns.
+Because Rubular doesn't understand `%{issue_ref}`, you can replace this by `#\d+` in testing, which matches only local issue references like `#123`.
## Change the pattern
For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`:
```
-issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)'
+issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)'
```
-For manual installs you can customize the pattern in [gitlab.yml][0].
+For manual installs you can customize the pattern in [gitlab.yml][0] using the `issue_closing_pattern` key.
-[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65
-[1]: http://rubular.com/r/Xmbexed1OJ \ No newline at end of file
+[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/gitlab.yml.example
+[1]: http://rubular.com/r/Xmbexed1OJ
diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md
index 54c1780c3ab..bd2c242afc2 100644
--- a/doc/customization/libravatar.md
+++ b/doc/customization/libravatar.md
@@ -2,7 +2,7 @@
GitLab by default supports [Gravatar](https://gravatar.com) avatar service.
Libravatar is a service which delivers your avatar (profile picture) to other websites and their API is
-[heavily based on gravatar](http://wiki.libravatar.org/api/).
+[heavily based on gravatar](https://wiki.libravatar.org/api/).
This means that it is not complicated to switch to Libravatar avatar service or even self hosted Libravatar server.
@@ -31,7 +31,7 @@ the configuration options as follows:
## Self-hosted
-If you are [running your own libravatar service](http://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration
+If you are [running your own libravatar service](https://wiki.libravatar.org/running_your_own/) the URL will be different in the configuration
but the important part is to provide the same placeholders so GitLab can parse the URL correctly.
For example, you host a service on `http://libravatar.example.com` the `plain_url` you need to supply in `gitlab.yml` is
@@ -63,7 +63,7 @@ Run `sudo gitlab-ctl reconfigure` for changes to take effect.
## Default URL for missing images
-[Libravatar supports different sets](http://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service.
+[Libravatar supports different sets](https://wiki.libravatar.org/api/) of `missing images` for emails not found on the Libravatar service.
In order to use a different set other than `identicon`, replace `&d=identicon` portion of the URL with another supported set.
For example, you can use `retro` set in which case the URL would look like: `plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=retro"`
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index c00d290371e..6101a71a8de 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -146,7 +146,7 @@ nginx
Apache httpd
-- [Explanation of Apache logs](http://httpd.apache.org/docs/2.2/logs.html).
+- [Explanation of Apache logs](https://httpd.apache.org/docs/2.2/logs.html).
- `/var/log/apache2/` contains error and output logs (on Ubuntu).
- `/var/log/httpd/` contains error and output logs (on RHEL).
diff --git a/doc/development/db_dump.md b/doc/development/db_dump.md
index 21f1b3edecd..e4ff72aa349 100644
--- a/doc/development/db_dump.md
+++ b/doc/development/db_dump.md
@@ -1,4 +1,4 @@
-# Importing a database dump into a staging enviroment
+# Importing a database dump into a staging environment
Sometimes it is useful to import the database from a production environment
into a staging environment for testing. The procedure below assumes you have
diff --git a/doc/development/profiling.md b/doc/development/profiling.md
index 80c86ef921e..e244ad4e881 100644
--- a/doc/development/profiling.md
+++ b/doc/development/profiling.md
@@ -4,11 +4,15 @@ To make it easier to track down performance problems GitLab comes with a set of
profiling tools, some of these are available by default while others need to be
explicitly enabled.
-## rack-mini-profiler
+## Sherlock
-This Gem is enabled by default in development only. It allows you to see the
-timings of the various components that made up a web request (e.g. the SQL
-queries executed and their execution timings).
+Sherlock is a custom profiling tool built into GitLab. Sherlock is _only_
+available when running GitLab in development mode _and_ when setting the
+environment variable `ENABLE_SHERLOCK` to a non empty value. For example:
+
+ ENABLE_SHERLOCK=1 bundle exec rails s
+
+Recorded transactions can be found by navigating to `/sherlock/transactions`.
## Bullet
@@ -21,36 +25,3 @@ starting GitLab. For example:
Bullet will log query problems to both the Rails log as well as the Chrome
console.
-
-## ActiveRecord Query Trace
-
-This Gem adds backtraces for every ActiveRecord query in the Rails console. This
-can be useful to track down where a query was executed. Because this Gem adds
-quite a bit of noise (5-10 extra lines per ActiveRecord query) it's disabled by
-default. To use this Gem you'll need to set `ENABLE_QUERY_TRACE` to a non empty
-file before starting GitLab. For example:
-
- ENABLE_QUERY_TRACE=true bundle exec rails s
-
-## rack-lineprof
-
-This is a Gem that can trace the execution time of code on a per line basis.
-Because this Gem can add quite a bit of overhead it's disabled by default. To
-enable it, set the environment variable `ENABLE_LINEPROF` to a non-empty value.
-For example:
-
- ENABLE_LINEPROF=true bundle exec rails s
-
-Once enabled you'll need to add a query string parameter to a request to
-actually profile code execution. The name of the parameter is `lineprof` and
-should be set to a regular expression (minus the starting/ending slash) used to
-select what files to profile. To profile all files containing "foo" somewhere in
-the path you'd use the following parameter:
-
- ?lineprof=foo
-
-Or when filtering for files containing "foo" and "bar" in their path:
-
- ?lineprof=foo|bar
-
-Once set the profiling output will be displayed in your terminal.
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index a4a980cf0e0..9f3fd69fc4e 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -9,7 +9,7 @@ bundle exec rake setup
```
The `setup` task is a alias for `gitlab:setup`.
-This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and fianlly it calls `db:seed_fu` to seed the database.
+This tasks calls `db:setup` to create the database, calls `add_limits_mysql` that adds limits to the database schema in case of a MySQL database and finally it calls `db:seed_fu` to seed the database.
Note: `db:setup` calls `db:seed` but this does nothing.
## Run tests
diff --git a/doc/development/shared_files.md b/doc/development/shared_files.md
new file mode 100644
index 00000000000..fcd905b54a4
--- /dev/null
+++ b/doc/development/shared_files.md
@@ -0,0 +1,33 @@
+# Shared files
+
+Historically, GitLab has been storing shared files in many different
+directories: `public/uploads`, `builds`, `tmp/repositories`, `tmp/rebase` (EE),
+etc. Having so many shared directories makes it difficult to deploy GitLab on
+shared storage (e.g. NFS). Working towards GitLab 9.0 we are consolidating
+these different directories under the `shared` directory.
+
+This means that if GitLab will start storing puppies in some future version
+then we should put them in `shared/puppies`. Temporary puppy files should be
+stored in `shared/tmp`.
+
+In the GitLab application code you can get the full path to the `shared`
+directory with `Gitlab.config.shared.path`.
+
+## What is not a 'shared file'
+
+Files that belong to only one process, or on only one server, should not go in
+`shared`. Examples include PID files and sockets.
+
+## Temporary files and shared storage
+
+Sometimes you create a temporary file on disk with the intention of it becoming
+'official'. For example you might be first streaming an upload from a user to
+disk in a temporary file so you can perform some checks on it. When the checks
+pass, you make the file official. In scenarios like this please follow these
+rules:
+
+- Store the temporary file under `shared/tmp`, i.e. on the same filesystem you
+ want the official file to be on.
+- Use move/rename operations when operating on the file instead of copy
+ operations where possible, because renaming a file is much faster than
+ copying it.
diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md
index 2d1d0fb4154..65cdd74bdb6 100644
--- a/doc/development/shell_commands.md
+++ b/doc/development/shell_commands.md
@@ -35,6 +35,16 @@ Gitlab::Popen.popen(%W(find /some/path -not -path /some/path -mmin +120 -delete)
This coding style could have prevented CVE-2013-4490.
+## Always use the configurable git binary path for git commands
+
+```ruby
+# Wrong
+system(*%W(git branch -d -- #{branch_name}))
+
+# Correct
+system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name}))
+```
+
## Bypass the shell by splitting commands into separate tokens
When we pass shell commands as a single string to Ruby, Ruby will let `/bin/sh` evaluate the entire string. Essentially, we are asking the shell to evaluate a one-line script. This creates a risk for shell injection attacks. It is better to split the shell command into tokens ourselves. Sometimes we use the scripting capabilities of the shell to change the working directory or set environment variables. All of this can also be achieved securely straight from Ruby
@@ -81,9 +91,9 @@ In the GitLab codebase, we avoid the option/argument ambiguity by _always_ using
```ruby
# Wrong
-system(*%W(git branch -d #{branch_name}))
+system(*%W(#{Gitlab.config.git.bin_path} branch -d #{branch_name}))
# Correct
-system(*%W(git branch -d -- #{branch_name}))
+system(*%W(#{Gitlab.config.git.bin_path} branch -d -- #{branch_name}))
```
This coding style could have prevented CVE-2013-4582.
@@ -94,9 +104,9 @@ Capturing the output of shell commands with backticks reads nicely, but you are
```ruby
# Wrong
-logs = `cd #{repo_dir} && git log`
+logs = `cd #{repo_dir} && #{Gitlab.config.git.bin_path} log`
# Correct
-logs, exit_status = Gitlab::Popen.popen(%W(git log), repo_dir)
+logs, exit_status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} log), repo_dir)
# Wrong
user = `whoami`
@@ -108,7 +118,7 @@ In other repositories, such as gitlab-shell you can also use `IO.popen`.
```ruby
# Safe IO.popen example
-logs = IO.popen(%W(git log), chdir: repo_dir) { |p| p.read }
+logs = IO.popen(%W(#{Gitlab.config.git.bin_path} log), chdir: repo_dir) { |p| p.read }
```
Note that unlike `Gitlab::Popen.popen`, `IO.popen` does not capture standard error.
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index 548c484bc08..0f2665a3bf7 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -7,7 +7,7 @@ Please explore webhooks as an option if you do not have filesystem access. For a
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](http://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks)
+[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
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index c565e90da2f..513ad69ec26 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -2,7 +2,7 @@
## Note
-We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](http://bugs.mysql.com/bug.php?id=65830) that [suggested](http://bugs.mysql.com/bug.php?id=50909) [fixes](http://bugs.mysql.com/bug.php?id=65830) [have](http://bugs.mysql.com/bug.php?id=63164).
+We do not recommend using MySQL due to various issues. For example, case [(in)sensitivity](https://dev.mysql.com/doc/refman/5.0/en/case-sensitivity.html) and [problems](https://bugs.mysql.com/bug.php?id=65830) that [suggested](https://bugs.mysql.com/bug.php?id=50909) [fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
## MySQL
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2e9ac7393e3..81edd8da2b8 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -38,6 +38,7 @@ The GitLab installation consists of setting up the following components:
1. Packages / Dependencies
1. Ruby
+1. Go
1. System Users
1. Database
1. Redis
@@ -62,7 +63,7 @@ up-to-date and install it.
Install the required packages (needed to compile Ruby and native extensions to Ruby gems):
- sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs
+ sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake nodejs
If you want to use Kerberos for user authentication, then install libkrb5-dev:
@@ -106,7 +107,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-The use of Ruby version managers such as [RVM](http://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby.
+The use of Ruby version managers such as [RVM](https://rvm.io/), [rbenv](https://github.com/sstephenson/rbenv) or [chruby](https://github.com/postmodern/chruby) with GitLab in production frequently leads to hard to diagnose problems. For example, GitLab Shell is called from OpenSSH and having a version manager can prevent pushing and pulling over SSH. Version managers are not supported and we strongly advise everyone to follow the instructions below to use a system Ruby.
Remove the old Ruby 1.8 if present
@@ -128,11 +129,10 @@ Install the Bundler Gem:
## 3. Go
-Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server.
-This is a small daemon written in Go.
-To install gitlab-git-http-server we need a Go compiler.
-The instructions below assume you use 64-bit Linux. You can find
-downloads for other platforms at the [Go download
+Since GitLab 8.0, Git HTTP requests are handled by gitlab-workhorse (formerly
+gitlab-git-http-server). This is a small daemon written in Go. To install
+gitlab-workhorse we need a Go compiler. The instructions below assume you
+use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
@@ -175,33 +175,53 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
## 6. Redis
- sudo apt-get install redis-server
+As of this writing, most Debian/Ubuntu distributions ship with Redis 2.2 or
+2.4. GitLab requires at least Redis 2.8.
- # Configure redis to use sockets
- sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig
+Ubuntu users [can use a PPA](https://launchpad.net/~chris-lea/+archive/ubuntu/redis-server)
+to install a recent version of Redis.
- # Disable Redis listening on TCP by setting 'port' to 0
- sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
+The following instructions cover building and installing Redis from scratch:
- # Enable Redis socket for default Debian / Ubuntu path
- echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
- # Grant permission to the socket to all members of the redis group
- echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf
+```sh
+# Build Redis
+wget http://download.redis.io/releases/redis-2.8.23.tar.gz
+tar xzf redis-2.8.23.tar.gz
+cd redis-2.8.23
+make
- # Create the directory which contains the socket
- mkdir /var/run/redis
- chown redis:redis /var/run/redis
- chmod 755 /var/run/redis
- # Persist the directory which contains the socket, if applicable
- if [ -d /etc/tmpfiles.d ]; then
- echo 'd /var/run/redis 0755 redis redis 10d -' | sudo tee -a /etc/tmpfiles.d/redis.conf
- fi
+# Install Redis
+cd utils
+sudo ./install_server.sh
- # Activate the changes to redis.conf
- sudo service redis-server restart
+# Configure redis to use sockets
+sudo cp /etc/redis/redis.conf /etc/redis/redis.conf.orig
- # Add git to the redis group
- sudo usermod -aG redis git
+# Disable Redis listening on TCP by setting 'port' to 0
+sed 's/^port .*/port 0/' /etc/redis/redis.conf.orig | sudo tee /etc/redis/redis.conf
+
+# Enable Redis socket for default Debian / Ubuntu path
+echo 'unixsocket /var/run/redis/redis.sock' | sudo tee -a /etc/redis/redis.conf
+
+# Grant permission to the socket to all members of the redis group
+echo 'unixsocketperm 770' | sudo tee -a /etc/redis/redis.conf
+
+# Create the directory which contains the socket
+mkdir /var/run/redis
+chown redis:redis /var/run/redis
+chmod 755 /var/run/redis
+
+# Persist the directory which contains the socket, if applicable
+if [ -d /etc/tmpfiles.d ]; then
+ echo 'd /var/run/redis 0755 redis redis 10d -' | sudo tee -a /etc/tmpfiles.d/redis.conf
+fi
+
+# Activate the changes to redis.conf
+sudo service redis_6379 start
+
+# Add git to the redis group
+sudo usermod -aG redis git
+```
## 7. GitLab
@@ -211,9 +231,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-1-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-3-stable gitlab
-**Note:** You can change `8-1-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -246,6 +266,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Change the permissions of the directory where CI build traces are stored
sudo chmod -R u+rwX builds/
+ # Change the permissions of the directory where CI artifacts are stored
+ sudo chmod -R u+rwX shared/artifacts/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -253,8 +276,8 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
nproc
# Enable cluster mode if you expect to have a high load instance
- # Ex. change amount of workers to 3 for 2GB RAM server
# Set the number of workers to at least the number of cores
+ # Ex. change amount of workers to 3 for 2GB RAM server
sudo -u git -H editor config/unicorn.rb
# Copy the example Rack attack config
@@ -295,7 +318,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Install Gems
-**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](http://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
+**Note:** As of bundler 1.5.2, you can invoke `bundle install -jN` (where `N` the number of your processor cores) and enjoy the parallel gems installation with measurable difference in completion time (~60% faster). Check the number of your cores with `nproc`. For more information check this [post](https://robots.thoughtbot.com/parallel-gem-installing-using-bundler). First make sure you have bundler >= 1.5.2 (run `bundle -v`) as it addresses some [issues](https://devcenter.heroku.com/changelog-items/411) that were [fixed](https://github.com/bundler/bundler/pull/2817) in 1.5.2.
# For PostgreSQL (note, the option says "without ... mysql")
sudo -u git -H bundle install --deployment --without development test mysql aws kerberos
@@ -310,7 +333,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
GitLab Shell is an SSH access and repository management software developed specially for GitLab.
# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed):
- sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.5] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
# By default, the gitlab-shell config is generated from your main GitLab config.
# You can review (and modify) the gitlab-shell config as follows:
@@ -318,18 +341,22 @@ GitLab Shell is an SSH access and repository management software developed speci
**Note:** If you want to use HTTPS, see [Using HTTPS](#using-https) for the additional steps.
-**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up gitlab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
+**Note:** Make sure your hostname can be resolved on the machine itself by either a proper DNS record or an additional line in /etc/hosts ("127.0.0.1 hostname"). This might be necessary for example if you set up GitLab behind a reverse proxy. If the hostname cannot be resolved, the final installation check will fail with "Check GitLab API access: FAILED. code: 401" and pushing commits will be rejected with "[remote rejected] master -> master (hook declined)".
-### Install gitlab-git-http-server
+### Install gitlab-workhorse
cd /home/git
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
- cd gitlab-git-http-server
- sudo -u git -H git checkout 0.3.0
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
+ cd gitlab-workhorse
+ sudo -u git -H git checkout 0.5.1
sudo -u git -H make
### Initialize Database and Activate Advanced Features
+ # Go to GitLab installation folder
+
+ cd /home/git/gitlab
+
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
# Type 'yes' to create the database tables.
@@ -346,11 +373,6 @@ The `secrets.yml` file stores encryption keys for sessions and secure variables.
Backup `secrets.yml` someplace safe, but don't store it in the same place as your database backups.
Otherwise your secrets are exposed if one of your backups is compromised.
-### Install schedules
-
- # Setup schedules
- sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production
-
### Install Init Script
Download the init script (will be `/etc/init.d/gitlab`):
@@ -494,7 +516,7 @@ See the [omniauth integration document](../integration/omniauth.md)
### Build your projects
GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you.
-Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
+Checkout the [GitLab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
### Custom Redis Connection
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index aa0d03b75bc..c0ccdd37458 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -109,8 +109,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/))
- Safari 7+ (known problem: required fields in html5 do not work)
- Opera (Latest released version)
-- IE 10+
-
-### Common UI problems with IE
-
-If you experience UI issues with Internet Explorer, please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file
+- Internet Explorer (IE) 10+ but please make sure that you have the `Compatibility View` mode disabled. \ No newline at end of file
diff --git a/doc/integration/README.md b/doc/integration/README.md
index eff39a626ae..2a9f76533b7 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -4,13 +4,16 @@ GitLab integrates with multiple third-party services to allow external issue tra
See the documentation below for details on how to configure these services.
+- [Jira](jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth.
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
+- [CAS](cas.md) Configure GitLab to sign in using CAS
- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
+- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html).
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 6a0fa4ce015..63432b04432 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -30,7 +30,7 @@ Bitbucket will generate an application ID and secret key for you to use.
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
diff --git a/doc/integration/cas.md b/doc/integration/cas.md
new file mode 100644
index 00000000000..e6b2071f193
--- /dev/null
+++ b/doc/integration/cas.md
@@ -0,0 +1,62 @@
+# CAS OmniAuth Provider
+
+To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL GitLab will supply to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for backchannel logout.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ name: "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:
+
+ ```
+ - { 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'} }
+ ```
+
+1. Change 'CAS_PATH' to the root of your CAS instance (ie. `cas`).
+
+1. If your CAS instance does not use default TGC lifetimes, update the `cas3.session_duration` to at least the current TGC maximum lifetime. To explicitly disable SLO, regardless of CAS settings, set this to 0.
+
+1. Save the configuration file.
+
+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/crowd.md b/doc/integration/crowd.md
index 2ecc8795ac1..40d93aef2a9 100644
--- a/doc/integration/crowd.md
+++ b/doc/integration/crowd.md
@@ -10,7 +10,7 @@ To enable the Crowd OmniAuth provider you must register your application with Cr
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
diff --git a/doc/integration/facebook.md b/doc/integration/facebook.md
new file mode 100644
index 00000000000..bc1f1673086
--- /dev/null
+++ b/doc/integration/facebook.md
@@ -0,0 +1,97 @@
+# Facebook OAuth2 OmniAuth Provider
+
+To enable the Facebook OmniAuth provider you must register your application with Facebook. Facebook will generate an app ID and secret key for you to use.
+
+1. Sign in to the [Facebook Developer Platform](https://developers.facebook.com/).
+
+1. Choose "My Apps" &gt; "Add a New App"
+
+1. Select the type "Website"
+
+1. Enter a name for your app. This can be anything. Consider something like "&lt;Organization&gt;'s GitLab" or "&lt;Your Name&gt;'s GitLab" or
+something else descriptive.
+
+1. Choose "Create New Facebook App ID"
+
+1. Select a Category, for example "Productivity"
+
+1. Choose "Create App ID"
+
+1. Enter the address of your GitLab installation at the bottom of the package
+
+ ![Facebook Website URL](facebook_website_url.png)
+
+1. Choose "Next"
+
+1. Choose "Skip Quick Start" in the upper right corner
+
+1. Choose "Settings" in the menu on the left
+
+1. Fill in a contact email for your app
+
+ ![Facebook App Settings](facebook_app_settings.png)
+
+1. Choose "Save Changes"
+
+1. Choose "Status & Review" in the menu on the left
+
+1. Change the switch on the right from No to Yes
+
+1. Choose "Confirm" when prompted to make the app public
+
+1. Choose "Dashboard" in the menu on the left
+
+1. Choose "Show" next to the hidden "App Secret"
+
+1. You should now see an app key and app secret (see screenshot). Keep this page open as you continue configuration.
+
+ ![Facebook API Keys](facebook_api_keys.png)
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "facebook",
+ "app_id" => "YOUR_APP_ID",
+ "app_secret" => "YOUR_APP_SECRET"
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```
+ - { name: 'facebook', app_id: 'YOUR_APP_ID',
+ app_secret: 'YOUR_APP_SECRET' }
+ ```
+
+1. Change 'YOUR_APP_ID' to the API key from Facebook page in step 10.
+
+1. Change 'YOUR_APP_SECRET' to the API secret from the Facebook page in step 10.
+
+1. Save the configuration file.
+
+1. Restart GitLab for the changes to take effect.
+
+On the sign in page there should now be a Facebook icon below the regular sign in form. Click the icon to begin the authentication process. Facebook will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
diff --git a/doc/integration/facebook_api_keys.png b/doc/integration/facebook_api_keys.png
new file mode 100644
index 00000000000..d6c44ac0f11
--- /dev/null
+++ b/doc/integration/facebook_api_keys.png
Binary files differ
diff --git a/doc/integration/facebook_app_settings.png b/doc/integration/facebook_app_settings.png
new file mode 100644
index 00000000000..30dd21e198a
--- /dev/null
+++ b/doc/integration/facebook_app_settings.png
Binary files differ
diff --git a/doc/integration/facebook_website_url.png b/doc/integration/facebook_website_url.png
new file mode 100644
index 00000000000..dc3088bb2fa
--- /dev/null
+++ b/doc/integration/facebook_website_url.png
Binary files differ
diff --git a/doc/integration/github.md b/doc/integration/github.md
index b64501c2aaa..a789d2c814f 100644
--- a/doc/integration/github.md
+++ b/doc/integration/github.md
@@ -32,7 +32,7 @@ GitHub will generate an application ID and secret key for you to use.
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 216f1f11a9b..80e3c0142a0 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -38,7 +38,7 @@ GitLab.com will generate an application ID and secret key for you to use.
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
diff --git a/doc/integration/google.md b/doc/integration/google.md
index e1c14c7c948..91e9b2495cc 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -35,7 +35,7 @@ To enable the Google OAuth2 OmniAuth provider you must register your application
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
new file mode 100644
index 00000000000..624601d0fac
--- /dev/null
+++ b/doc/integration/jira.md
@@ -0,0 +1,113 @@
+# GitLab Jira integration
+
+GitLab can be configured to interact with Jira.
+Configuration happens via username and password.
+Connecting to a Jira server via CAS is not possible.
+
+Each project can be configured to connect to a different Jira instance, configuration is explained [here](#configuration).
+If you have one Jira instance you can pre-fill the settings page with a default template. To configure the template [see external issue tracker document](external-issue-tracker.md#service-template)).
+
+Once the project is connected to Jira, you can reference and close the issues in Jira directly from GitLab.
+
+
+## Table of Contents
+
+* [Referencing Jira Issues from GitLab](#referencing-jira-issues)
+* [Closing Jira Issues from GitLab](#closing-jira-issues)
+* [Configuration](#configuration)
+
+### Referencing Jira Issues
+
+When GitLab project has Jira issue tracker configured and enabled, mentioning Jira issue in GitLab will automatically add a comment in Jira issue with the link back to GitLab. This means that in comments in merge requests and commits referencing an issue, eg. `PROJECT-7`, will add a comment in Jira issue in the format:
+
+
+```
+ USER mentioned this issue in LINK_TO_THE_MENTION
+```
+
+* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
+* `LINK_TO_THE_MENTION` Link to the origin of mention with a name of the entity where Jira issue was mentioned.
+Can be commit or merge request.
+
+
+![example of mentioning or closing the Jira issue](jira_issue_reference.png)
+
+
+### Closing Jira Issues
+
+Jira issues can be closed directly from GitLab by using trigger words, eg. `Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and merge requests.
+When a commit which contains the trigger word in the commit message is pushed, GitLab will add a comment in the mentioned Jira issue.
+
+For example, for project named PROJECT in Jira, we implemented a new feature and created a merge request in GitLab.
+
+This feature was requested in Jira issue PROJECT-7. Merge request in GitLab contains the improvement and in merge request description we say that this merge request `Closes PROJECT-7` issue.
+
+Once this merge request is merged, Jira issue will be automatically closed with a link to the commit that resolved the issue.
+
+![A Git commit that causes the Jira issue to be closed](merge_request_close_jira.png)
+
+
+![The GitLab integration user leaves a comment on Jira](jira_service_close_issue.png)
+
+
+## Configuration
+
+### Configuring JIRA
+
+We need to create a user in JIRA which will have access to all projects that need to integrate with GitLab.
+Login to your JIRA instance as admin and under Administration go to User Management and create a new user.
+As an example, we'll create a user named `gitlab` and add it to `jira-developers` group.
+
+**It is important that the user `gitlab` has write-access to projects in JIRA**
+
+### Configuring GitLab
+
+### GitLab 7.8 EE and up with JIRA v6.x
+
+To enable JIRA integration in a project, navigate to the project Settings page and go to Services. Here you will find JIRA.
+
+Fill in the required details on the page:
+
+![Jira service page](jira_service_page.png)
+
+* `description` A name for the issue tracker (to differentiate between instances, for instance).
+* `project url` The URL to the JIRA project which is being linked to this GitLab project.
+* `issues url` The URL to the JIRA project issues overview for the project that is linked to this GitLab project.
+* `new issue url` This is the URL to create a new issue in JIRA for the project linked to this GitLab project.
+* `api url` The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`, i.e. `https://jira.example.com/rest/api/2`.
+* `username` The username of the user created in [configuring JIRA step](#configuring-jira).
+* `password` The password of the user created in [configuring JIRA step](#configuring-jira).
+* `Jira issue transition` This is the id of a transition that moves issues to a closed state. You can find this number under [JIRA workflow administration, see screenshot](jira_workflow_screenshot.png). By default, this id is `2`. (In the example image, this is `2` as well)
+
+After saving the configuration, your GitLab project will be able to interact with the linked JIRA project.
+
+
+### GitLab 6.x-7.7 with JIRA v6.x
+
+**Note: GitLab 7.8 and up contain various integration improvements. We strongly recommend upgrading.**
+
+
+In `gitlab.yml` enable [JIRA issue tracker section by uncommenting the lines](https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115).
+This will make sure that all issues within GitLab are pointing to the JIRA issue tracker.
+
+We can also enable JIRA service that will allow us to interact with JIRA issues.
+
+For example, we can close issues in JIRA by a commit in GitLab.
+
+Go to project settings page and fill in the project name for the JIRA project:
+
+![Set the JIRA project name in GitLab to 'NEW'](jira_project_name.png)
+
+Next, go to the services page and find JIRA.
+
+![Jira services page](jira_service.png)
+
+1. Tick the active check box to enable the service.
+1. Supply the url to JIRA server, for example http://jira.sample
+1. Supply the username of a user we created under `Configuring JIRA` section, for example `gitlab`
+1. Supply the password of the user
+1. Optional: supply the JIRA api version, default is version
+1. Optional: supply the JIRA issue transition ID (issue transition to closed). This is dependant on JIRA settings, default is 2
+1. Save
+
+Now we should be able to interact with JIRA issues.
diff --git a/doc/integration/jira_issue_reference.png b/doc/integration/jira_issue_reference.png
new file mode 100644
index 00000000000..15739a22dc7
--- /dev/null
+++ b/doc/integration/jira_issue_reference.png
Binary files differ
diff --git a/doc/integration/jira_project_name.png b/doc/integration/jira_project_name.png
new file mode 100644
index 00000000000..5986fdb63fb
--- /dev/null
+++ b/doc/integration/jira_project_name.png
Binary files differ
diff --git a/doc/integration/jira_service.png b/doc/integration/jira_service.png
new file mode 100644
index 00000000000..1f6628c4371
--- /dev/null
+++ b/doc/integration/jira_service.png
Binary files differ
diff --git a/doc/integration/jira_service_close_issue.png b/doc/integration/jira_service_close_issue.png
new file mode 100644
index 00000000000..67dfc6144c4
--- /dev/null
+++ b/doc/integration/jira_service_close_issue.png
Binary files differ
diff --git a/doc/integration/jira_service_page.png b/doc/integration/jira_service_page.png
new file mode 100644
index 00000000000..69ec44e826f
--- /dev/null
+++ b/doc/integration/jira_service_page.png
Binary files differ
diff --git a/doc/integration/jira_workflow_screenshot.png b/doc/integration/jira_workflow_screenshot.png
new file mode 100644
index 00000000000..8635a32eb68
--- /dev/null
+++ b/doc/integration/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 9b7d8fa3969..845f588f913 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -13,6 +13,12 @@ An LDAP user who is allowed to change their email on the LDAP server can [take o
We recommend against using GitLab LDAP integration if your LDAP users are allowed to change their 'mail', 'email' or 'userPrincipalName' attribute on the LDAP server.
+If a user is deleted from the LDAP server, they will be blocked in GitLab as well.
+Users will be immediately blocked from logging in. However, there is an LDAP check
+cache time of one hour. The means users that are already logged in or are using Git
+over SSH will still be able to access GitLab for up to one hour. Manually block
+the user in the GitLab Admin area to immediately block all access.
+
## Configuring GitLab for LDAP integration
To enable GitLab LDAP integration you need to add your LDAP server settings in `/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml`.
@@ -71,7 +77,7 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
# Filter LDAP users
#
- # Format: RFC 4515 http://tools.ietf.org/search/rfc4515
+ # Format: RFC 4515 https://tools.ietf.org/search/rfc4515
# Ex. (employeeType=developer)
#
# Note: GitLab does not support omniauth-ldap's custom filter syntax.
@@ -145,7 +151,7 @@ If multiple LDAP email attributes are present, e.g. `mail: foo@bar.com` and `ema
## Using an LDAP filter to limit access to your GitLab server
If you want to limit all GitLab access to a subset of the LDAP users on your LDAP server you can set up an LDAP user filter.
-The filter must comply with [RFC 4515](http://tools.ietf.org/search/rfc4515).
+The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515).
```ruby
# For omnibus packages; new LDAP server syntax
@@ -192,4 +198,4 @@ Not supported by GitLab's configuration options.
When setting `method: ssl`, the underlying authentication method used by
`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
the LDAP server before any LDAP-protocol data is exchanged but no validation of
-the LDAP server's SSL certificate is performed. \ No newline at end of file
+the LDAP server's SSL certificate is performed.
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index c5cecbc2f2d..f2b1721fc03 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -36,7 +36,7 @@ If you want to change these settings:
```
gitlab_rails['omniauth_enabled'] = true
gitlab_rails['omniauth_allow_single_sign_on'] = false
- gitlab_rails['block_auto_created_users'] = true
+ gitlab_rails['omniauth_block_auto_created_users'] = true
```
* **For installations from source**
@@ -73,8 +73,9 @@ Now we can choose one or more of the Supported Providers below to continue confi
- [Bitbucket](bitbucket.md)
- [GitLab.com](gitlab.md)
- [Google](google.md)
-- [Shibboleth](shibboleth.md)
+- [Facebook](facebook.md)
- [Twitter](twitter.md)
+- [Shibboleth](shibboleth.md)
- [SAML](saml.md)
- [Crowd](crowd.md)
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
new file mode 100644
index 00000000000..a301d1a613c
--- /dev/null
+++ b/doc/integration/recaptcha.md
@@ -0,0 +1,23 @@
+# reCAPTCHA
+
+GitLab leverages [Google's reCAPTCHA](https://www.google.com/recaptcha/intro/index.html)
+to protect against spam and abuse. GitLab displays the CAPTCHA form on the sign-up page
+to confirm that a real user, not a bot, is attempting to create an account.
+
+## Configuration
+
+To use reCAPTCHA, first you must create a site and private key.
+
+1. Go to the URL: https://www.google.com/recaptcha/admin
+
+2. Fill out the form necessary to obtain reCAPTCHA keys.
+
+3. Login to your GitLab server, with administrator credentials.
+
+4. Go to Applications Settings on Admin Area (`admin/application_settings`)
+
+5. Fill all recaptcha fields with keys from previous steps
+
+6. Check the `Enable reCAPTCHA` checkbox
+
+7. Save the configuration.
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 4aa6dbe758a..1632e42f701 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -14,7 +14,7 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
@@ -38,7 +38,8 @@ First configure SAML 2.0 support in GitLab, then register the GitLab application
idp_sso_target_url: 'https://login.example.com/idp',
issuer: 'https://gitlab.example.com',
name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
- }
+ },
+ "label" => "Company Login" # optional label for SAML login button, defaults to "Saml"
}
]
```
@@ -79,4 +80,4 @@ On the sign in page there should now be a SAML button below the regular sign in
If you see a "500 error" in GitLab when you are redirected back from the SAML sign in page, this likely indicates that GitLab could not get the email address for the SAML user.
-Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username.
+Make sure the IdP provides a claim containing the user's email address, using claim name 'email' or 'mail'. The email will be used to automatically generate the GitLab username. \ No newline at end of file
diff --git a/doc/integration/twitter.md b/doc/integration/twitter.md
index 1350c8f693c..52ed4a22339 100644
--- a/doc/integration/twitter.md
+++ b/doc/integration/twitter.md
@@ -37,7 +37,7 @@ To enable the Twitter OmniAuth provider you must register your application with
sudo editor /etc/gitlab/gitlab.rb
```
- For instalations from source:
+ For installations from source:
```sh
cd /home/git/gitlab
diff --git a/doc/legal/corporate_contributor_license_agreement.md b/doc/legal/corporate_contributor_license_agreement.md
index 13bf15fcf45..7b94506c297 100644
--- a/doc/legal/corporate_contributor_license_agreement.md
+++ b/doc/legal/corporate_contributor_license_agreement.md
@@ -22,4 +22,4 @@ You accept and agree to the following terms and conditions for Your present and
8. It is your responsibility to notify GitLab B.V. when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with GitLab B.V..
-This text is licensed under the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/legal/individual_contributor_license_agreement.md b/doc/legal/individual_contributor_license_agreement.md
index 72b01433dd0..f97c252fd7c 100644
--- a/doc/legal/individual_contributor_license_agreement.md
+++ b/doc/legal/individual_contributor_license_agreement.md
@@ -22,4 +22,4 @@ You accept and agree to the following terms and conditions for Your present and
8. You agree to notify GitLab B.V. of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
-This text is licensed under the [Creative Commons Attribution 3.0 License](http://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
+This text is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/) and the original source is the Google Open Source Programs Office.
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index ac3851f8c95..bc8e7d155e7 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -43,7 +43,7 @@ You can also use other rich text files in GitLab. You might have to install a de
## Newlines
-GFM honors the markdown specification in how [paragraphs and line breaks are handled](http://daringfireball.net/projects/markdown/syntax#p).
+GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
@@ -72,14 +72,14 @@ do_this_and_do_that_and_another_thing
GFM will autolink almost any URL you copy and paste into your text.
- * http://www.google.com
+ * https://www.google.com
* https://google.com/
* ftp://ftp.us.debian.org/debian/
* smb://foo/bar/baz
* irc://irc.freenode.net/gitlab
* http://localhost:3000
-* http://www.google.com
+* https://www.google.com
* https://google.com/
* ftp://ftp.us.debian.org/debian/
* smb://foo/bar/baz
@@ -390,7 +390,7 @@ There are two ways to create links, inline-style and reference-style.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
- [link text itself]: http://www.reddit.com
+ [link text itself]: https://www.reddit.com
[I'm an inline-style link](https://www.google.com)
@@ -406,7 +406,7 @@ Some text to show that the reference links can follow later.
[arbitrary case-insensitive reference text]: https://www.mozilla.org
[1]: http://slashdot.org
-[link text itself]: http://www.reddit.com
+[link text itself]: https://www.reddit.com
**Note**
@@ -583,5 +583,5 @@ By including colons in the header row, you can align the text within that column
## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
-- The [Markdown Syntax Guide](http://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
+- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
diff --git a/doc/operations/moving_repositories.md b/doc/operations/moving_repositories.md
new file mode 100644
index 00000000000..39086b7a251
--- /dev/null
+++ b/doc/operations/moving_repositories.md
@@ -0,0 +1,180 @@
+# Moving repositories managed by GitLab
+
+Sometimes you need to move all repositories managed by GitLab to
+another filesystem or another server. In this document we will look
+at some of the ways you can copy all your repositories from
+`/var/opt/gitlab/git-data/repositories` to `/mnt/gitlab/repositories`.
+
+We will look at three scenarios: the target directory is empty, the
+target directory contains an outdated copy of the repositories, and
+how to deal with thousands of repositories.
+
+**Each of the approaches we list can/will overwrite data in the
+target directory `/mnt/gitlab/repositories`. Do not mix up the
+source and the target.**
+
+## Target directory is empty: use a tar pipe
+
+If the target directory `/mnt/gitlab/repositories` is empty the
+simplest thing to do is to use a tar pipe. This method has low
+overhead and tar is almost always already installed on your system.
+However, it is not possible to resume an interrupted tar pipe: if
+that happens then all data must be copied again.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+ tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to see progress, replace `-xf` with `-xvf`.
+
+### Tar pipe to another server
+
+You can also use a tar pipe to copy data to another server. If your
+'git' user has SSH access to the newserver as 'git@newserver', you
+can pipe the data through SSH.
+
+```
+# As the git user
+tar -C /var/opt/gitlab/git-data/repositories -cf - -- . |\
+ ssh git@newserver tar -C /mnt/gitlab/repositories -xf -
+```
+
+If you want to compress the data before it goes over the network
+(which will cost you CPU cycles) you can replace `ssh` with `ssh -C`.
+
+## The target directory contains an outdated copy of the repositories: use rsync
+
+If the target directory already contains a partial / outdated copy
+of the repositories it may be wasteful to copy all the data again
+with tar. In this scenario it is better to use rsync. This utility
+is either already installed on your system or easily installable
+via apt, yum etc.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+ /mnt/gitlab/repositories
+```
+
+The `/.` in the command above is very important, without it you can
+easily get the wrong directory structure in the target directory.
+If you want to see progress, replace `-a` with `-av`.
+
+### Single rsync to another server
+
+If the 'git' user on your source system has SSH access to the target
+server you can send the repositories over the network with rsync.
+
+```
+# As the 'git' user
+rsync -a --delete /var/opt/gitlab/git-data/repositories/. \
+ git@newserver:/mnt/gitlab/repositories
+```
+
+## Thousands of Git repositories: use one rsync per repository
+
+Every time you start an rsync job it has to inspect all files in
+the source directory, all files in the target directory, and then
+decide what files to copy or not. If the source or target directory
+has many contents this startup phase of rsync can become a burden
+for your GitLab server. In cases like this you can make rsync's
+life easier by dividing its work in smaller pieces, and sync one
+repository at a time.
+
+In addition to rsync we will use [GNU
+Parallel](http://www.gnu.org/software/parallel/). This utility is
+not included in GitLab so you need to install it yourself with apt
+or yum. Also note that the GitLab scripts we used below were added
+in GitLab 8.1.
+
+** This process does not clean up repositories at the target location that no
+longer exist at the source. ** If you start using your GitLab instance with
+`/mnt/gitlab/repositories`, you need to run `gitlab-rake gitlab:cleanup:repos`
+after switching to the new repository storage directory.
+
+### Parallel rsync for all repositories known to GitLab
+
+This will sync repositories with 10 rsync processes at a time. We keep
+track of progress so that the transfer can be restarted if necessary.
+
+First we create a new directory, owned by 'git', to hold transfer
+logs. We assume the directory is empty before we start the transfer
+procedure, and that we are the only ones writing files in it.
+
+```
+# Omnibus
+sudo mkdir /var/opt/gitlab/transfer-logs
+sudo chown git:git /var/opt/gitlab/transfer-logs
+
+# Source
+sudo -u git -H mkdir /home/git/transfer-logs
+```
+
+We seed the process with a list of the directories we want to copy.
+
+```
+# Omnibus
+sudo -u git sh -c 'gitlab-rake gitlab:list_repos > /var/opt/gitlab/transfer-logs/all-repos-$(date +%s).txt'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H sh -c 'bundle exec rake gitlab:list_repos > /home/git/transfer-logs/all-repos-$(date +%s).txt'
+```
+
+Now we can start the transfer. The command below is idempotent, and
+the number of jobs done by GNU Parallel should converge to zero. If it
+does not some repositories listed in all-repos-1234.txt may have been
+deleted/renamed before they could be copied.
+
+```
+# Omnibus
+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/git-data/repositories \
+ /mnt/gitlab/repositories
+'
+
+# Source
+cd /home/git/gitlab
+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/repositories \
+ /mnt/gitlab/repositories
+`
+```
+
+### Parallel rsync only for repositories with recent activity
+
+Suppose you have already done one sync that started after 2015-10-1 12:00 UTC.
+Then you might only want to sync repositories that were changed via GitLab
+_after_ that time. You can use the 'SINCE' variable to tell 'rake
+gitlab:list_repos' to only print repositories with recent activity.
+
+```
+# Omnibus
+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 \
+ /var/opt/gitlab/git-data/repositories \
+ /mnt/gitlab/repositories
+
+# Source
+cd /home/git/gitlab
+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 \
+ /home/git/repositories \
+ /mnt/gitlab/repositories
+```
diff --git a/doc/operations/unicorn.md b/doc/operations/unicorn.md
index 31b432cd411..bad61151bda 100644
--- a/doc/operations/unicorn.md
+++ b/doc/operations/unicorn.md
@@ -52,7 +52,7 @@ leak memory, probably because it does not handle user requests.)
To make these memory leaks manageable, GitLab comes with the
[unicorn-worker-killer gem](https://github.com/kzk/unicorn-worker-killer). This
-gem [monkey-patches](http://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
+gem [monkey-patches](https://en.wikipedia.org/wiki/Monkey_patch) the Unicorn
workers to do a memory self-check after every 16 requests. If the memory of the
Unicorn worker exceeds a pre-set limit then the worker process exits. The
Unicorn master then automatically replaces the worker process.
@@ -78,9 +78,9 @@ threshold is a random value between 200 and 250 MB. The master process (PID
```
One other thing that stands out in the log snippet above, taken from
-Gitlab.com, is that 'worker 4' was serving requests for only 23 seconds. This
+GitLab.com, is that 'worker 4' was serving requests for only 23 seconds. This
is a normal value for our current GitLab.com setup and traffic.
The high frequency of Unicorn memory restarts on some GitLab sites can be a
source of confusion for administrators. Usually they are a [red
-herring](http://en.wikipedia.org/wiki/Red_herring).
+herring](https://en.wikipedia.org/wiki/Red_herring).
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index 7a6a1958445..1be78ac1823 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -6,8 +6,11 @@ If a user is both in a project group and in the project itself, the highest perm
If a user is a GitLab administrator they receive all permissions.
+On public projects the Guest role is not enforced.
+All users will be able to create issues, leave comments, and pull or download the project code.
+
To add or import a user, you can follow the [project users and members
-documentation](doc/workflow/add-user/add-user.md).
+documentation](../workflow/add-user/add-user.md).
## Project
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index bd439f7c6f3..6e22ea7b72a 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -1,44 +1,59 @@
# Public access
-GitLab allows you to open selected projects to be accessed **publicly** or **internally**.
+GitLab allows you to change your projects' visibility in order be accessed
+**publicly** or **internally**.
-Projects with either of these visibility levels will be listed in the [public access directory](/public).
+Projects with either of these visibility levels will be listed in the
+public access directory (`/public` under your GitLab instance).
+Here is the [GitLab.com example](https://gitlab.com/public).
Internal projects will only be available to authenticated users.
-## Public projects
+## Visibility of projects
+
+### Public projects
Public projects can be cloned **without any** authentication.
-It will also be listed on the [public access directory](/public).
+They will also be listed on the public access directory (`/public`).
-**Any logged in user** will have [Guest](../permissions/permissions) permissions on the repository.
+**Any logged in user** will have [Guest](../permissions/permissions)
+permissions on the repository.
-## Internal projects
+### Internal projects
Internal projects can be cloned by any logged in user.
-It will also be listed on the [public access directory](/public) for logged in users.
+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) permissions on
+the repository.
-## How to change project visibility
+### How to change project visibility
-1. Go to your project dashboard
-1. Click on the "Edit" tab
-1. Change "Visibility Level"
+1. Go to your project's **Settings**
+1. Change "Visibility Level" to either Public, Internal or Private
## Visibility of users
-The public page of users, located at `/u/username` is visible if either:
+The public page of a user, located at `/u/username`, is always visible whether
+you are logged in or not.
+
+When visiting the public page of a user, you can only see the projects which
+you are privileged to.
-- You are logged in.
-- You are logged out, and the target user is authorized to (is Guest, Reporter, etc.) at least one public project.
+## Visibility of groups
-Otherwise, you will be redirected to the sign in page.
+The public page of a group, located at `/groups/groupname`, is always visible
+to everyone.
-When visiting the public page of an user, you will only see listed projects which you can view yourself.
+Logged out users will be able to see the description and the avatar of the
+group as well as all public projects belonging to that group.
## Restricting the use of public or internal projects
-In the Admin area under Settings you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users.
+In the Admin area under **Settings** (`/admin/application_settings`), you can
+restrict the use of visibility levels for users when they create a project or a
+snippet. This is useful to prevent people exposing their repositories to public
+by accident. The restricted visibility settings do not apply to admin users.
diff --git a/doc/raketasks/README.md b/doc/raketasks/README.md
index a8dc5c24df2..cc8a22cd003 100644
--- a/doc/raketasks/README.md
+++ b/doc/raketasks/README.md
@@ -1,10 +1,11 @@
# Rake tasks
- [Backup restore](backup_restore.md)
+- [Check](check.md)
- [Cleanup](cleanup.md)
- [Features](features.md)
- [Maintenance](maintenance.md) and self-checks
- [User management](user_management.md)
- [Web hooks](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 \ No newline at end of file
+- [Rebuild authorized_keys file](http://doc.gitlab.com/ce/raketasks/maintenance.html#rebuild-authorized_keys-file) task for administrators
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 06f582dcee8..cdd6652b7b0 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -29,7 +29,8 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
-uploads (attachments), repositories. Use a comma to specify several options at the same time.
+uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects).
+Use a comma to specify several options at the same time.
```
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
@@ -152,6 +153,49 @@ with the name of your bucket:
}
```
+### Uploading to locally mounted shares
+
+You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
+using the [`Local`](https://github.com/fog/fog-local#usage) storage provider.
+The directory pointed to by the `local_root` key **must** be owned by the `git`
+user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and
+`SMB`) or the user that you are executing the backup tasks under (for omnibus
+packages, this is the `git` user).
+
+The `backup_upload_remote_directory` **must** be set in addition to the
+`local_root` key. This is the sub directory inside the mounted directory that
+backups will be copied to, and will be created if it does not exist. If the
+directory that you want to copy the tarballs to is the root of your mounted
+directory, just use `.` instead.
+
+For omnibus packages:
+
+```ruby
+gitlab_rails['backup_upload_connection'] = {
+ :provider => 'Local',
+ :local_root => '/mnt/backups'
+}
+
+# The directory inside the mounted folder to copy backups to
+# Use '.' to store them in the root directory
+gitlab_rails['backup_upload_remote_directory'] = 'gitlab_backups'
+```
+
+For installations from source:
+
+```yaml
+ backup:
+ # snip
+ upload:
+ # Fog storage connection settings, see http://fog.io/storage/ .
+ connection:
+ provider: Local
+ local_root: '/mnt/backups'
+ # The directory inside the mounted folder to copy backups to
+ # Use '.' to store them in the root directory
+ remote_directory: 'gitlab_backups'
+```
+
## Backup archive permissions
The backup archives created by GitLab (123456_gitlab_backup.tar) will have owner/group git:git and 0600 permissions by default.
@@ -273,9 +317,6 @@ sudo gitlab-rake gitlab:backup:restore BACKUP=1393513186
# Start GitLab
sudo gitlab-ctl start
-# Create satellites
-sudo gitlab-rake gitlab:satellites:create
-
# Check GitLab
sudo gitlab-rake gitlab:check SANITIZE=true
```
@@ -359,8 +400,8 @@ If you are using backup restore procedures you might encounter the following war
```
psql:/var/opt/gitlab/backups/db/database.sql:22: ERROR: must be owner of extension plpgsql
-psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurences)
-psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurences)
+psql:/var/opt/gitlab/backups/db/database.sql:2931: WARNING: no privileges could be revoked for "public" (two occurrences)
+psql:/var/opt/gitlab/backups/db/database.sql:2933: WARNING: no privileges were granted for "public" (two occurrences)
```
diff --git a/doc/raketasks/check.md b/doc/raketasks/check.md
new file mode 100644
index 00000000000..3ff3fee6a40
--- /dev/null
+++ b/doc/raketasks/check.md
@@ -0,0 +1,63 @@
+# Check Rake Tasks
+
+## Repository Integrity
+
+Even though Git is very resilient and tries to prevent data integrity issues,
+there are times when things go wrong. The following Rake tasks intend to
+help GitLab administrators diagnose problem repositories so they can be fixed.
+
+There are 3 things that are checked to determine integrity.
+
+1. Git repository file system check ([git fsck](https://git-scm.com/docs/git-fsck)).
+ This step verifies the connectivity and validity of objects in the repository.
+1. Check for `config.lock` in the repository directory.
+1. Check for any branch/references lock files in `refs/heads`.
+
+It's important to note that the existence of `config.lock` or reference locks
+alone do not necessarily indicate a problem. Lock files are routinely created
+and removed as Git and GitLab perform operations on the repository. They serve
+to prevent data integrity issues. However, if a Git operation is interrupted these
+locks may not be cleaned up properly.
+
+The following symptoms may indicate a problem with repository integrity. If users
+experience these symptoms you may use the rake tasks described below to determine
+exactly which repositories are causing the trouble.
+
+- Receiving an error when trying to push code - `remote: error: cannot lock ref`
+- A 500 error when viewing the GitLab dashboard or when accessing a specific project.
+
+### Check all GitLab repositories
+
+This task loops through all repositories on the GitLab server and runs the
+3 integrity checks described previously.
+
+```
+# omnibus-gitlab
+sudo gitlab-rake gitlab:repo:check
+
+# installation from source
+bundle exec rake gitlab:repo:check RAILS_ENV=production
+```
+
+### Check repositories for a specific user
+
+This task checks all repositories that a specific user has access to. This is important
+because sometimes you know which user is experiencing trouble but you don't know
+which project might be the cause.
+
+If the rake task is executed without brackets at the end, you will be prompted
+to enter a username.
+
+```bash
+# omnibus-gitlab
+sudo gitlab-rake gitlab:user:check_repos
+sudo gitlab-rake gitlab:user:check_repos[<username>]
+
+# installation from source
+bundle exec rake gitlab:user:check_repos RAILS_ENV=production
+bundle exec rake gitlab:user:check_repos[<username>] RAILS_ENV=production
+```
+
+Example output:
+
+![gitlab:user:check_repos output](check_repos_output.png)
diff --git a/doc/raketasks/check_repos_output.png b/doc/raketasks/check_repos_output.png
new file mode 100644
index 00000000000..916b1685101
--- /dev/null
+++ b/doc/raketasks/check_repos_output.png
Binary files differ
diff --git a/doc/raketasks/list_repos.md b/doc/raketasks/list_repos.md
new file mode 100644
index 00000000000..476428eb4f5
--- /dev/null
+++ b/doc/raketasks/list_repos.md
@@ -0,0 +1,30 @@
+# Listing repository directories
+
+You can print a list of all Git repositories on disk managed by
+GitLab with the following command:
+
+```
+# Omnibus
+sudo gitlab-rake gitlab:list_repos
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production
+```
+
+If you only want to list projects with recent activity you can pass
+a date with the 'SINCE' environment variable. The time you specify
+is parsed by the Rails [TimeZone#parse
+function](http://api.rubyonrails.org/classes/ActiveSupport/TimeZone.html#method-i-parse).
+
+```
+# Omnibus
+sudo gitlab-rake gitlab:list_repos SINCE='Sep 1 2015'
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production SINCE='Sep 1 2015'
+```
+
+Note that the projects listed are NOT sorted by activity; they use
+the default ordering of the GitLab Rails application.
diff --git a/doc/release/README.md b/doc/release/README.md
index 1342b90f3b3..52eca7c02a6 100644
--- a/doc/release/README.md
+++ b/doc/release/README.md
@@ -1,4 +1,8 @@
-GitLab has the following updates:
+## Release cycle
+
+Since 2011 a minor or major version of GitLab is released on the 22nd of every month. Patch and security releases are published when needed. New features are detailed on the [blog](https://about.gitlab.com/blog/) and in the [changelog](CHANGELOG). Features that will likely be in the next releases can be found on the [direction page](https://about.gitlab.com/direction/).
+
+## Release process documentation
- [Monthly release](monthly.md), every month on the 22nd.
- [Patch release](patch.md), if there are serious regressions.
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
index bd8a67d1d85..907c19e65a0 100644
--- a/doc/release/monthly.md
+++ b/doc/release/monthly.md
@@ -25,68 +25,88 @@ If the release is falling behind immediately warn the team.
## Create an overall issue and follow it
-Create issue for GitLab CE project(internal). Name it "Release x.x.x" for easier searching.
-Replace the dates with actual dates based on the number of workdays before the release.
-All steps from issue template are explained below
+Create an issue in the GitLab CE project. Name it "Release x.x" and tag it with
+the `release` label for easier searching. Replace the dates with actual dates
+based on the number of workdays before the release. All steps from issue
+template are explained below:
```
-Xth: (7 working days before the 22nd)
+### Xth: (7 working days before the 22nd)
-- [ ] Triage the omnibus-gitlab milestone
+- [ ] Triage the [Omnibus milestone]
-Xth: (6 working days before the 22nd)
+### Xth: (6 working days before the 22nd)
-- [ ] Merge CE master in to EE master via merge request (#LINK)
- [ ] Determine QA person and notify this person
- [ ] Check the tasks in [how to rc1 guide](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/release/howto_rc1.md) and delegate tasks if necessary
-- [ ] Create CE, EE, CI RC1 versions (#LINK)
-- [ ] Build RC1 packages (EE first) (#LINK)
+- [ ] Merge CE `master` into EE `master` via merge request (#LINK)
+- [ ] Create CE and EE RC1 versions (#LINK)
+- [ ] Build RC1 packages
-Xth: (5 working days before the 22nd)
+### Xth: (5 working days before the 22nd)
- [ ] Do QA and fix anything coming out of it (#LINK)
-- [ ] Close the omnibus-gitlab milestone
-- [ ] Prepare the blog post (#LINK)
+- [ ] Close the [Omnibus milestone]
+- [ ] Prepare the [blog post]
-Xth: (4 working days before the 22nd)
+### Xth: (4 working days before the 22nd)
-- [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package)
-- [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package)
-- [ ] Create regression issues (CE, CI) (#LINK)
-- [ ] Tweet about rc1 (#LINK), proposed text:
+- [ ] Update GitLab.com with RC1
+- [ ] Create the regression issue in the CE issue tracker:
-> GitLab x.x.0.rc1 is available https://packages.gitlab.com/gitlab/unstable Use at your own risk. Please link regressions issues from LINK_TO_REGRESSION_ISSUE
+ ```
+ This is a meta issue to index possible regressions in this monthly release
+ and any patch versions.
-Xth: (3 working days before the 22nd)
+ Please do not raise or discuss issues directly in this issue but link to
+ issues that might warrant a patch release. If there is a Merge Request
+ that fixes the issue, please link to that as well.
-- [ ] Merge CE stable branch into EE stable branch
+ Please only post one regression issue and/or merge request per comment.
+ Comments will be updated by the release manager as they are addressed.
+ ```
-Xth: (2 working days before the 22nd)
+- [ ] Tweet about RC1 release:
-- [ ] Check that everyone is mentioned on the blog post using `@all` (the reviewer should have done this one working day ago)
-- [ ] Check that MVP is added to the mvp page (source/mvp/index.html in www-gitlab-com)
+ ```
+ GitLab x.y.0.rc1 is available: https://packages.gitlab.com/gitlab/unstable
+ Use at your own risk. Please link regressions issues from
+ LINK_TO_REGRESSION_ISSUE
+ ```
-Xth: (1 working day before the 22nd)
+### Xth: (3 working days before the 22nd)
-- [ ] Merge CE stable into EE stable
-- [ ] Create CE, EE, CI release candidates (#LINK) (hopefully final ones with the same commit as the release tomorrow)
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] Check that everyone is mentioned on the [blog post] using `@all`
+
+### Xth: (2 working days before the 22nd)
+
+- [ ] Check that MVP is added to the [MVP page]
+
+### Xth: (1 working day before the 22nd)
+
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] Create CE and EE release candidates
- [ ] Create Omnibus tags and build packages for the latest release candidates
-- [ ] Update GitLab.com with the latest RC (#LINK)
-- [ ] Update ci.gitLab.com with the latest RC (#LINK)
+- [ ] Update GitLab.com with the latest RC
-22nd before 1200 CET:
+### 22nd before 1200 CET:
Release before 1200 CET / 2AM PST, to make sure the majority of our users
get the new version on the 22nd and there is sufficient time in the European
workday to quickly fix any issues.
-- [ ] Merge CE stable into EE stable (#LINK)
-- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools) (#LINK)
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] Create the 'x.y.0' tag with the [release tools](https://dev.gitlab.org/gitlab/release-tools)
- [ ] Create the 'x.y.0' version on version.gitlab.com
-- [ ] Try to do before 1100 CET: Create and push omnibus tags for x.y.0 (will auto-release the packages) (#LINK)
-- [ ] Try to do before 1200 CET: Publish the release blog post (#LINK)
-- [ ] Tweet about the release (blog post) (#LINK)
-- [ ] Schedule a second tweet of the release announcement with the same text at 1800 CET / 8AM PST
+- [ ] Try to do before 1100 CET: Create and push Omnibus tags for x.y.0 (will auto-release the packages)
+- [ ] Try to do before 1200 CET: Publish the release [blog post]
+- [ ] Tweet about the release
+- [ ] Schedule a second Tweet of the release announcement with the same text at 1800 CET / 8AM PST
+
+[Omnibus milestone]: LINK_TO_OMNIBUS_MILESTONE
+[blog post]: LINK_TO_WIP_BLOG_POST
+[MVP page]: https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/source/mvp/index.html
```
- - -
@@ -139,7 +159,7 @@ Please do not raise issues directly in this issue but link to issues that might
The decision to create a patch release or not is with the release manager who is assigned to this issue.
The release manager will comment here about the plans for patch releases.
-Assign the issue to the release manager and at mention all members of gitlab core team. If there are any known bugs in the release add them immediately.
+Assign the issue to the release manager and at mention all members of GitLab core team. If there are any known bugs in the release add them immediately.
## Tweet about RC1
@@ -156,7 +176,7 @@ Tweet about the RC release:
1. Also check the CI changelog
1. Add a proposed tweet text to the blog post WIP MR description.
1. Create a WIP MR for the blog post
-1. Make sure merge request title starts with `WIP` so it can not be accidently merged until ready.
+1. Make sure merge request title starts with `WIP` so it can not be accidentally merged until ready.
1. Ask Dmitriy (or a team member with OS X) to add screenshots to the WIP MR.
1. Decide with core team who will be the MVP user.
1. Create WIP MR for adding MVP to MVP page on website
diff --git a/doc/release/patch.md b/doc/release/patch.md
index 6aa11b283df..3022e375aca 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -1,21 +1,46 @@
# Things to do when doing a patch release
-NOTE: This is a guide for GitLab developers. If you are trying to install GitLab see the latest stable [installation guide](install/installation.md) and if you are trying to upgrade, see the [upgrade guides](update).
+NOTE: This is a guide for GitLab developers. If you are trying to install GitLab
+see the latest stable [installation guide](install/installation.md) and if you
+are trying to upgrade, see the [upgrade guides](update).
## When to do a patch release
-Do a patch release when there is a critical regression that needs to be addresses before the next monthly release.
-
-Otherwise include it in the monthly release and note there was a regression fix in the release announcement.
+Patch releases are done as-needed in order to fix regressions in the current
+major release that cannot or should not wait until the next major release.
+What's included and when to release is at the discretion of the release manager.
## Release Procedure
+### Create a patch issue
+
+Create an issue in the GitLab CE project. Name it "Release x.y.z", tag it with
+the `release` label, and assign it to the milestone of the corresponding major
+release.
+
+Use the following template:
+
+```
+- Picked into respective `stable` branches:
+- [ ] Merge `x-y-stable` into `x-y-stable-ee`
+- [ ] release-tools: `x.y.z`
+- gitlab-omnibus
+ - [ ] `x.y.z+ee.0`
+ - [ ] `x.y.z+ce.0`
+- [ ] Deploy
+- [ ] Add patch notice to [x.y regressions]()
+- [ ] [Blog post]()
+- [ ] [Tweet]()
+- [ ] Add entry to version.gitlab.com
+```
+
+Update the issue with links to merge requests that need to be/have been picked
+into the `stable` branches.
+
### Preparation
1. Verify that the issue can be reproduced
1. Note in the 'GitLab X.X regressions' that you will create a patch
-1. Create an issue on private GitLab development server
-1. Name the issue "Release X.X.X CE and X.X.X EE", this will make searching easier
1. Fix the issue on a feature branch, do this on the private GitLab development server
1. If it is a security issue, then assign it to the release manager and apply a 'security' label
1. Consider creating and testing workarounds
@@ -25,7 +50,6 @@ Otherwise include it in the monthly release and note there was a regression fix
1. For EE, update the CHANGELOG-EE if it is EE specific fix. Otherwise, merge the stable CE branch and add to CHANGELOG-EE "Merge community edition changes for version X.X.X"
1. Merge CE stable branch into EE stable branch
-
### Bump version
Get release tools
@@ -54,4 +78,4 @@ bundle exec rake release["x.x.x"]
1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
1. Create the 'x.y.0' version on version.gitlab.com
1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
-1. Create a new patch release issue for the next potential release \ No newline at end of file
+1. Create a new patch release issue for the next potential release
diff --git a/doc/release/security.md b/doc/release/security.md
index 60bcfbb6da5..b1a62b333e6 100644
--- a/doc/release/security.md
+++ b/doc/release/security.md
@@ -8,7 +8,7 @@ Do a security release when there is a critical issue that needs to be addresses
## Security vulnerability disclosure
-Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](http://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
+Please report suspected security vulnerabilities in private to <support@gitlab.com>, also see the [disclosure section on the GitLab.com website](https://about.gitlab.com/disclosure/). Please do NOT create publicly viewable issues for suspected security vulnerabilities.
## Release Procedure
@@ -25,7 +25,7 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
1. Send tweets about the release from `@gitlabhq`
1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
-1. Add the security researcher to the [Security Researcher Acknowledgments list](http://about.gitlab.com/vulnerability-acknowledgements/)
+1. Add the security researcher to the [Security Researcher Acknowledgments list](https://about.gitlab.com/vulnerability-acknowledgements/)
1. Thank the security researcher in an email for their cooperation
1. Update the blog post and the CHANGELOG when we receive the CVE number
diff --git a/doc/security/README.md b/doc/security/README.md
index 473f3632dcd..f34c792d005 100644
--- a/doc/security/README.md
+++ b/doc/security/README.md
@@ -4,4 +4,7 @@
- [Rack attack](rack_attack.md)
- [Web Hooks and insecure internal web services](webhooks.md)
- [Information exclusivity](information_exclusivity.md)
-- [Reset your root password](reset_root_password.md) \ No newline at end of file
+- [Reset your root password](reset_root_password.md)
+- [User File Uploads](user_file_uploads.md)
+- [How we manage the CRIME vulnerability](crime_vulnerability.md)
+- [Enforce Two-Factor authentication](two_factor_authentication.md)
diff --git a/doc/security/crime_vulnerability.md b/doc/security/crime_vulnerability.md
new file mode 100644
index 00000000000..94ba5d1375d
--- /dev/null
+++ b/doc/security/crime_vulnerability.md
@@ -0,0 +1,63 @@
+# How we manage the TLS protocol CRIME vulnerability
+
+> CRIME ("Compression Ratio Info-leak Made Easy") is a security exploit against
+secret web cookies over connections using the HTTPS and SPDY protocols that also
+use data compression. When used to recover the content of secret
+authentication cookies, it allows an attacker to perform session hijacking on an
+authenticated web session, allowing the launching of further attacks.
+([CRIME](https://en.wikipedia.org/w/index.php?title=CRIME&oldid=692423806))
+
+### Description
+
+The TLS Protocol CRIME Vulnerability affects compression over HTTPS, therefore
+it warns against using SSL Compression (for example gzip) or SPDY which
+optionally uses compression as well.
+
+GitLab supports both gzip and [SPDY][ngx-spdy] and mitigates the CRIME
+vulnerability by deactivating gzip when HTTPS is enabled. You can see the
+sources of the files in question:
+
+* [Source installation NGINX file][source-nginx]
+* [Omnibus installation NGINX file][omnibus-nginx]
+
+Although SPDY is enabled in Omnibus installations, CRIME relies on compression
+(the 'C') and the default compression level in NGINX's SPDY module is 0
+(no compression).
+
+### Nessus
+
+The Nessus scanner, [reports a possible CRIME vulnerability][nessus] in GitLab
+similar to the following format:
+
+```
+Description
+
+This remote service has one of two configurations that are known to be required for the CRIME attack:
+SSL/TLS compression is enabled.
+TLS advertises the SPDY protocol earlier than version 4.
+
+...
+
+Output
+
+The following configuration indicates that the remote service may be vulnerable to the CRIME attack:
+SPDY support earlier than version 4 is advertised.
+```
+
+From the report above it is important to note that Nessus is only checking if
+TLS advertises the SPDY protocol earlier than version 4, it does not perform an
+attack nor does it check if compression is enabled. With just this approach, it
+cannot tell that SPDY's compression is disabled and not subject to the CRIME
+vulnerability.
+
+### References
+
+* Nginx ["Module ngx_http_spdy_module"][ngx-spdy]
+* Tenable Network Security, Inc. ["Transport Layer Security (TLS) Protocol CRIME Vulnerability"][nessus]
+* Wikipedia contributors, ["CRIME"][wiki-crime] Wikipedia, The Free Encyclopedia
+
+[source-nginx]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/gitlab-ssl
+[omnibus-nginx]: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-cookbooks/gitlab/templates/default/nginx-gitlab-http.conf.erb
+[ngx-spdy]: http://nginx.org/en/docs/http/ngx_http_spdy_module.html
+[nessus]: https://www.tenable.com/plugins/index.php?view=single&id=62565
+[wiki-crime]: https://en.wikipedia.org/wiki/CRIME
diff --git a/doc/security/two_factor_authentication.md b/doc/security/two_factor_authentication.md
new file mode 100644
index 00000000000..4e25a1fdc3f
--- /dev/null
+++ b/doc/security/two_factor_authentication.md
@@ -0,0 +1,38 @@
+# Enforce Two-factor Authentication (2FA)
+
+Two-factor Authentication (2FA) provides an additional level of security to your
+users' GitLab account. Once enabled, in addition to supplying their username and
+password to login, they'll be prompted for a code generated by an application on
+their phone.
+
+You can read more about it here:
+[Two-factor Authentication (2FA)](doc/profile/two_factor_authentication.md)
+
+## Enabling 2FA
+
+Users on GitLab, can enable it without any admin's intervention. If you want to
+enforce everyone to setup 2FA, you can choose from two different ways:
+
+ 1. Enforce on next login
+ 2. Suggest on next login, but allow a grace period before enforcing.
+
+In the Admin area under **Settings** (`/admin/application_settings`), look for
+the "Sign-in Restrictions" area, where you can configure both.
+
+If you want 2FA enforcement to take effect on next login, change the grace
+period to `0`
+
+## Disabling 2FA for everyone
+
+There may be some special situations where you want to disable 2FA for everyone
+even when forced 2FA is disabled. There is a rake task for that:
+
+```
+# use this command if you've installed GitLab with the Omnibus package
+sudo gitlab-rake gitlab:two_factor:disable_for_all_users
+
+# if you've installed GitLab from source
+sudo -u git -H bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
+```
+
+**IMPORTANT: this is a permanent and irreversible action. Users will have to reactivate 2FA from scratch if they want to use it again.**
diff --git a/doc/security/user_file_uploads.md b/doc/security/user_file_uploads.md
new file mode 100644
index 00000000000..98493d33b00
--- /dev/null
+++ b/doc/security/user_file_uploads.md
@@ -0,0 +1,11 @@
+# User File Uploads
+
+Images attached to issues, merge requests or comments do not require authentication
+to be viewed if someone knows the direct URL. This direct URL contains a random
+32-character ID that prevents unauthorized people from guessing the URL to an
+image containing sensitive information. We don't enable authentication because
+these images need to be visible in the body of notification emails, which are
+often read from email clients that are not authenticated with GitLab, like
+Outlook, Apple Mail, or the Mail app on your mobile device.
+
+Note that non-image attachments do require authentication to be viewed.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index b6b8000af4e..77eb53427e2 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -9,7 +9,7 @@ already has one by running the following command:
cat ~/.ssh/id_rsa.pub
```
-If you see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the `ssh-keygen` step.
+If you see a long string starting with `ssh-rsa`, you can skip the `ssh-keygen` step.
Note: It is a best practice to use a password for an SSH key, but it is not
required and you can skip creating a password by pressing enter. Note that
@@ -20,8 +20,9 @@ To generate a new SSH key, use the following command:
ssh-keygen -t rsa -C "$your_email"
```
This command will prompt you for a location and filename to store the key
-pair and for a password. When prompted for the location and filename, you
-can press enter to use the default.
+pair and for a password. When prompted for the location and filename, just
+press enter to use the default. If you use a different name, the key will not
+be used automatically.
Use the command below to show your public key:
```bash
@@ -29,10 +30,10 @@ cat ~/.ssh/id_rsa.pub
```
Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
-user profile. Please copy the complete key starting with `ssh-` and ending
+user profile. Please copy the complete key starting with `ssh-rsa` and ending
with your username and host.
-To copy your public key to the clipboard, use code below. Depending on your
+To copy your public key to the clipboard, use the code below. Depending on your
OS you'll need to use a different command:
**Windows:**
@@ -78,11 +79,11 @@ Deploy keys can be shared between projects, you just need to add them to each pr
### Eclipse
-How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
+How to add your ssh key to Eclipse: https://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
## Tip: Non-default OpenSSH key file names or locations
-If, for whatever reason, you decide to specify a non-default location and filename for your Gitlab SSH key pair, you must configure your SSH client to find your Gitlab SSH private key for connections to your Gitlab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
+If, for whatever reason, you decide to specify a non-default location and filename for your GitLab SSH key pair, you must configure your SSH client to find your GitLab SSH private key for connections to your GitLab server (perhaps gitlab.com). For OpenSSH clients, this is handled in the `~/.ssh/config` file with a stanza similar to the following:
```
#
@@ -97,7 +98,7 @@ User mygitlabusername
Another example
```
#
-# Our company's internal Gitlab server
+# Our company's internal GitLab server
#
Host my-gitlab.company.com
RSAAuthentication yes
@@ -109,4 +110,4 @@ Note in the gitlab.com example above a username was specified to override the de
Due to the wide variety of SSH clients and their very large number of configuration options, further explanation of these topics is beyond the scope of this document.
Public SSH keys need to be unique, as they will bind to your account. Your SSH key is the only identifier you'll
-have when pushing code via SSH. That's why it needs to uniquely map to a single user.
+have when pushing code via SSH. That's why it needs to uniquely map to a single user. \ No newline at end of file
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 5cb05b13b3e..49f98ded046 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,6 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
System hooks can be used, e.g. for logging or changing information in a LDAP server.
@@ -17,6 +17,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -33,6 +34,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_destroy",
"name": "Underscore",
"owner_email": "johnsmith@gmail.com",
@@ -44,11 +46,48 @@ X-Gitlab-Event: System Hook
}
```
+**Project renamed:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_rename",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "jsmith/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
+**Project transferred:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_transfer",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "scores/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
**New Team Member:**
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_team",
"project_access": "Master",
"project_id": 74,
@@ -67,6 +106,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_team",
"project_access": "Master",
"project_id": 74,
@@ -85,6 +125,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_create",
"name": "John Smith",
@@ -97,6 +138,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_destroy",
"name": "John Smith",
@@ -110,6 +152,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_create",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -122,6 +165,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_destroy",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -133,6 +177,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -147,6 +192,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -161,6 +207,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_group",
"group_access": "Master",
"group_id": 78,
@@ -176,6 +223,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_group",
"group_access": "Master",
"group_id": 78,
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 b34fb12da6f..4516a102084 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
@@ -47,7 +47,7 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
-curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
+curl --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
cd ruby-2.1.6
./configure --disable-install-rdoc
make
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
new file mode 100644
index 00000000000..46dfa2232b4
--- /dev/null
+++ b/doc/update/8.1-to-8.2.md
@@ -0,0 +1,188 @@
+# From 8.1 to 8.2
+
+**NOTE:** GitLab 8.0 introduced several significant changes related to
+installation and configuration which *are not duplicated here*. Be sure you're
+already running a working version of at least 8.0 before proceeding with this
+guide.
+
+### 0. Double-check your Git version
+
+**This notice applies only to /usr/local/bin/git**
+
+If you compiled Git from source on your GitLab server then please double-check
+that you are using a version that protects against CVE-2014-9390. For six
+months after this vulnerability became known the GitLab installation guide
+still contained instructions that would install the outdated, 'vulnerable' Git
+version 2.1.2.
+
+Run the following command to get your current Git version:
+
+```sh
+/usr/local/bin/git --version
+```
+
+If you see 'No such file or directory' then you did not install Git according
+to the outdated instructions from the GitLab installation guide and you can go
+to the next step 'Stop server' below.
+
+If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4,
+v2.2.1 or newer. You can use the [instructions in the GitLab source
+installation
+guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies)
+to install a newer version of Git.
+
+### 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-2-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-2-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch
+sudo -u git -H git checkout v2.6.8
+```
+
+### 5. Replace gitlab-git-http-server with 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
+sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
+cd gitlab-workhorse
+sudo -u git -H git checkout 0.4.2
+sudo -u git -H make
+```
+
+Update the GitLab 'default' file.
+
+```
+cd /home/git/gitlab
+test -e /etc/default/gitlab && \
+ sudo sed -i.pre-8.2 's/^\([^=]*\)gitlab_git_http_server/\1gitlab_workhorse/' /etc/default/gitlab
+```
+
+Make sure that you also update your **NGINX configuration** to use
+the new gitlab-workhorse.socket file.
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+# Update init.d script
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 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-1-stable:config/gitlab.yml.example origin/8-2-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+View changes between the previous recommended Nginx configuration and the
+current one:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-1-stable:lib/support/nginx/gitlab-ssl origin/8-2-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-1-stable:lib/support/nginx/gitlab origin/8-2-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-2-stable/lib/support/init.d/gitlab.default.example#L34
+
+### 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.1)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.0 to 8.1](8.0-to-8.1.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.
+
+## Troubleshooting
+
+### "You appear to have cloned an empty repository."
+
+See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
new file mode 100644
index 00000000000..3748941b781
--- /dev/null
+++ b/doc/update/8.2-to-8.3.md
@@ -0,0 +1,200 @@
+# From 8.2 to 8.3
+
+**NOTE:** GitLab 8.0 introduced several significant changes related to
+installation and configuration which *are not duplicated here*. Be sure you're
+already running a working version of at least 8.0 before proceeding with this
+guide.
+
+### 0. Double-check your Git version
+
+**This notice applies only to /usr/local/bin/git**
+
+If you compiled Git from source on your GitLab server then please double-check
+that you are using a version that protects against CVE-2014-9390. For six
+months after this vulnerability became known the GitLab installation guide
+still contained instructions that would install the outdated, 'vulnerable' Git
+version 2.1.2.
+
+Run the following command to get your current Git version:
+
+```sh
+/usr/local/bin/git --version
+```
+
+If you see 'No such file or directory' then you did not install Git according
+to the outdated instructions from the GitLab installation guide and you can go
+to the next step 'Stop server' below.
+
+If you see a version string then it should be v1.8.5.6, v1.9.5, v2.0.5, v2.1.4,
+v2.2.1 or newer. You can use the [instructions in the GitLab source installation
+guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies)
+to install a newer version of Git.
+
+### 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-3-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-3-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.9
+```
+
+### 5. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires [Go 1.5](https://golang.org/dl)
+which should already be on your system from GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout 0.5.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
+
+# 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-2-stable:config/gitlab.yml.example origin/8-3-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+GitLab 8.3 introduces major changes in the NGINX configuration.
+Because all HTTP requests pass through gitlab-workhorse now a lot of
+directives need to be removed from NGINX. During future upgrades there
+should be much less changes in the NGINX configuration because of
+this.
+
+View changes between the previous recommended Nginx configuration and the
+current one:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-2-stable:lib/support/nginx/gitlab-ssl origin/8-3-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-2-stable:lib/support/nginx/gitlab origin/8-3-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-3-stable/lib/support/init.d/gitlab.default.example#L34
+
+#### Init script
+
+We updated the init script for GitLab in order to pass new
+configuration options to gitlab-workhorse. We let gitlab-workhorse
+connect to the Rails application via a Unix domain socket and we tell
+it where the 'public' directory of GitLab is.
+
+```
+cd /home/git/gitlab
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 8. Use Redis v2.8.0+
+
+Previous versions of GitLab allowed Redis versions >= 2.0 to be used, but
+GitLab 8.3 uses Sidekiq 4.0, which requires Redis 2.8. You can check your Redis version
+with the following command:
+
+ redis-cli info | grep redis_version
+
+If you need to upgrade, see the [installation guide for Redis](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-3-stable/doc/install/installation.md#6-redis).
+
+### 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.2)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.1 to 8.2](8.1-to-8.2.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.
+
+## Troubleshooting
+
+### "You appear to have cloned an empty repository."
+
+See the [7.14 to 8.0 update guide](7.14-to-8.0.md#troubleshooting).
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index a596ea38456..a7de5648c0e 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -60,6 +60,9 @@ sudo -u git -H python mysql-postgresql-converter/db_converter.py gitlabhq_produc
sudo -u git -H ed -s db/database.sql < mysql-postgresql-converter/move_drop_indexes.ed
# Compress database backup
+# Warning: If you have Gitlab 7.12.0 or older skip this step and import the database.sql directly into the backup with:
+# sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql
+# The compressed databasedump is not supported at 7.12.0 and older.
sudo -u git -H gzip db/database.sql
# Replace the MySQL dump in TIMESTAMP_gitlab_backup.tar.
@@ -71,4 +74,5 @@ sudo -u git -H tar rf TIMESTAMP_gitlab_backup.tar db/database.sql.gz
# Done! TIMESTAMP_gitlab_backup.tar can now be restored into a Postgres GitLab
# installation.
+# See https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/raketasks/backup_restore.md for more information about backups.
```
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 593722eb01f..c19ee49f9e0 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -6,7 +6,8 @@ For example from 7.14.0 to 7.14.3, also see the [semantic versioning specificati
### 0. Backup
It's useful to make a backup just in case things go south:
-(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab user on the database version)
+(With MySQL, this may require granting "LOCK TABLES" privileges to the GitLab
+user on the database version)
```bash
cd /home/git/gitlab
@@ -15,19 +16,23 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 1. Stop server
- sudo service gitlab stop
+```bash
+sudo service gitlab stop
+```
### 2. Get latest code for the stable branch
+In the commands below, replace `LATEST_TAG` with the latest GitLab tag you want
+to update to, for example `v8.0.3`. Use `git tag -l 'v*.[0-9]' --sort='v:refname'`
+to see a list of all tags. Make sure to update patch versions only (check your
+current version with `cat VERSION`).
+
```bash
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
sudo -u git -H git checkout LATEST_TAG -b LATEST_TAG
```
-Replace `LATEST_TAG` with the latest GitLab tag you want to update to, for example `v8.0.3`.
-Use `git tag -l 'v*.[0-9]' --sort='v:refname'` to see a list of all tags.
-Make sure to update patch versions only (check your current version with `cat VERSION`)
### 3. Update gitlab-shell to the corresponding version
@@ -37,12 +42,20 @@ sudo -u git -H git fetch
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
```
-### 4. Install libs, migrations, etc.
+### 4. Update gitlab-workhorse to the corresponding version
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch
+sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION`
+```
+
+### 5. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
-#PostgreSQL
+# PostgreSQL
sudo -u git -H bundle install --without development test mysql --deployment
# MySQL
@@ -52,19 +65,25 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
```
-### 5. Start application
+### 6. Start application
- sudo service gitlab start
- sudo service nginx restart
+```bash
+sudo service gitlab start
+sudo service nginx restart
+```
-### 6. Check application status
+### 7. 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
+```bash
+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 with:
- sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```bash
+sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+```
If all items are green, then congratulations upgrade complete!
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index ef99a69f60a..6420d65cf1b 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -57,6 +57,9 @@ X-Gitlab-Event: Push Hook
"name": "Jordi Mallach",
"email": "jordi@softcatala.org"
}
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
},
{
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
@@ -66,10 +69,14 @@ X-Gitlab-Event: Push Hook
"author": {
"name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)"
- }
+ },
+ "added": ["CHANGELOG"],
+ "modified": ["app/controller/application.rb"],
+ "removed": []
}
],
"total_commits_count": 4
+
}
```
@@ -181,7 +188,7 @@ X-Gitlab-Event: Note Hook
{
"object_kind": "note",
"user": {
- "name": "Adminstrator",
+ "name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
@@ -334,7 +341,7 @@ X-Gitlab-Event: Note Hook
{
"object_kind": "note",
"user": {
- "name": "Adminstrator",
+ "name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 5b8d72dfd34..3651b55f438 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,5 +13,10 @@
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
- [Web Editor](web_editor.md)
+- [Releases](releases.md)
+- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)
+- [Merge When Build Succeeds](merge_when_build_succeeds.md)
+- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
+- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
new file mode 100644
index 00000000000..fb26ee04393
--- /dev/null
+++ b/doc/workflow/award_emoji.png
Binary files differ
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index f608674faf6..8965e5b3654 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -7,7 +7,7 @@ This allows a wide variety of branching strategies and workflows.
Almost all of these are an improvement over the methods used before git.
But many organizations end up with a workflow that is not clearly defined, overly complex or not integrated with issue tracking systems.
Therefore we propose the GitLab flow as clearly defined set of best practices.
-It combines [feature driven development](http://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
+It combines [feature driven development](https://en.wikipedia.org/wiki/Feature-driven_development) and [feature branches](http://martinfowler.com/bliki/FeatureBranch.html) with issue tracking.
Organizations coming to git from other version control systems frequently find it hard to develop an effective workflow.
This article describes the GitLab flow that integrates the git workflow with an issue tracking system.
@@ -26,7 +26,7 @@ After getting used to these three steps the branching model becomes the challeng
Since many organizations new to git have no conventions how to work with it, it can quickly become a mess.
The biggest problem they run into is that many long running branches that each contain part of the changes are around.
People have a hard time figuring out which branch they should develop on or deploy to production.
-Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html)
+Frequently the reaction to this problem is to adopt a standardized pattern such as [git flow](http://nvie.com/posts/a-successful-git-branching-model/) and [GitHub flow](http://scottchacon.com/2011/08/31/github-flow.html).
We think there is still room for improvement and will detail a set of practices we call GitLab flow.
## Git flow and its problems
@@ -91,7 +91,7 @@ This workflow where commits only flow downstream ensures that everything has bee
If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch.
If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
-An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](http://teatro.io/).
+An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/).
## Release branches with GitLab flow
@@ -104,7 +104,7 @@ By branching as late as possible you minimize the time you have to apply bug fix
After a release branch is announced, only serious bug fixes are included in the release branch.
If possible these bug fixes are first merged into master and then cherry-picked into the release branch.
This way you can't forget to cherry-pick them into master and encounter the same bug on subsequent releases.
-This is called an 'upstream first' policy that is also practiced by [Google](http://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](http://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo).
+This is called an 'upstream first' policy that is also practiced by [Google](https://www.chromium.org/chromium-os/chromiumos-design-docs/upstream-first) and [Red Hat](https://www.redhat.com/about/news/archive/2013/5/a-community-for-using-openstack-with-red-hat-rdo).
Every time a bug-fix is included in a release branch the patch version is raised (to comply with [Semantic Versioning](http://semver.org/)) by setting a new tag.
Some projects also have a stable branch that points to the same commit as the latest released branch.
In this flow it is not common to have a production branch (or git flow master branch).
@@ -200,7 +200,7 @@ And to understand a change in context one can always look at the merge commit th
After you merge multiple commits from a feature branch into the master branch this is harder to undo.
If you would have squashed all the commits into one you could have just reverted this commit but as we indicated you should not rebase commits after they are pushed.
-Fortunately [reverting a merge made some time ago](http://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
+Fortunately [reverting a merge made some time ago](https://git-scm.com/blog/2010/03/02/undoing-merges.html) can be done with git.
This however, requires having specific merge commits for the commits your want to revert.
If you revert a merge and you change your mind, revert the revert instead of merging again since git will not allow you to merge the code again otherwise.
@@ -215,7 +215,7 @@ With git you can also rebase your feature branch commits to order them after the
This prevents creating a merge commit when merging master into your feature branch and creates a nice linear history.
However, just like with squashing you should never rebase commits you have pushed to a remote server.
This makes it impossible to rebase work in progress that you already shared with your team which is something we recommend.
-When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](http://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/).
+When using rebase to keep your feature branch updated you [need to resolve similar conflicts again and again](https://blogs.atlassian.com/2013/10/git-team-workflows-merge-or-rebase/).
You can reuse recorded resolutions (rerere) sometimes, but without rebasing you only have to solve the conflicts one time and you’re set.
There has to be a better way to avoid many merge commits.
@@ -244,13 +244,12 @@ Developing software happen in small messy steps and it is OK to have your histor
You can use tools to view the network graphs of commits and understand the messy history that created your code.
If you rebase code the history is incorrect, and there is no way for tools to remedy this because they can't deal with changing commit identifiers.
-## Voting on merge requests
+## Award emojis on issues and merge requests
-![Voting slider in GitLab](voting_slider.png)
+![Emoji bar in GitLab](award_emoji.png)
-It is common to voice approval or disapproval by using +1 or -1 emoticons.
-In GitLab the +1 and -1 are aggregated and shown at the top of the merge request.
-As a rule of thumb anything that doesn't have two times more +1's than -1's is suspect and should not be merged yet.
+It is common to voice approval or disapproval by using +1 or -1. In GitLab you
+can use emojis to give a virtual high five on issues and merge requests.
## Pushing and removing branches
@@ -307,7 +306,7 @@ When initiating a feature branch, always start with an up to date master to bran
If you know beforehand that your work absolutely depends on another branch you can also branch from there.
If you need to merge in another branch after starting explain the reason in the merge commit.
If you have not pushed your commits to a shared location yet you can also rebase on master or another feature branch.
-Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](http://lwn.net/Articles/328438/).
+Do not merge in upstream if your code will work and merge cleanly without doing so, Linus even says that [you should never merge in upstream at random points, only at major releases](https://lwn.net/Articles/328438/).
Merging only when needed prevents creating merge commits in your feature branch that later end up littering the master history.
### References
diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md
index 7ccf06fbd60..18e5d950866 100644
--- a/doc/workflow/importing/README.md
+++ b/doc/workflow/importing/README.md
@@ -1,13 +1,17 @@
# Migrating projects to a GitLab instance
1. [Bitbucket](import_projects_from_bitbucket.md)
-2. [GitHub](import_projects_from_github.md)
-3. [GitLab.com](import_projects_from_gitlab_com.md)
-4. [FogBugz](import_projects_from_fogbugz.md)
-4. [SVN](migrating_from_svn.md)
+1. [GitHub](import_projects_from_github.md)
+1. [GitLab.com](import_projects_from_gitlab_com.md)
+1. [FogBugz](import_projects_from_fogbugz.md)
+1. [SVN](migrating_from_svn.md)
-### Note
-* If you'd like to migrate from a self-hosted GitLab instance to GitLab.com, you can copy your repos by changing the remote and pushing to the new server; but issues and merge requests can't be imported.
+In addition to the specific migration documentation above, you can import any
+Git repository via HTTP from the New Project page. Be aware that if the
+repository is too large the import can timeout.
+
+### Migrating from self-hosted GitLab to GitLab.com
+
+You can copy your repos by changing the remote and pushing to the new server;
+but issues and merge requests can't be imported.
-* You can import any Git repository via HTTP from the New Project page.
-If the repository is too large, it can timeout.
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index 2d77c6d1172..2027a055c37 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -14,7 +14,7 @@ If you want to import from a GitHub Enterprise instance, you need to use GitLab
![Importer page](github_importer/importer.png)
-* To import a project, you can simple click "Add". The importer will import your repository and issues. Once the importer is done, a new GitLab project will be created with your imported data.
+* To import a project, you can simple click "Add". The importer will import your repository, issues, and pull requests. Once the importer is done, a new GitLab project will be created with your imported data.
### Note
-When you import your projects from GitHub, it is not possible to keep your labels and milestones. We are working on improving this in the near future.
+When you import your projects from GitHub, it is not possible to keep your labels, milestones, and cross-repository pull requests. We are working on improving this in the near future.
diff --git a/doc/workflow/importing/import_projects_from_gitlab_com.md b/doc/workflow/importing/import_projects_from_gitlab_com.md
index f4c4e955d46..1117db98e7e 100644
--- a/doc/workflow/importing/import_projects_from_gitlab_com.md
+++ b/doc/workflow/importing/import_projects_from_gitlab_com.md
@@ -2,12 +2,12 @@
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://doc.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)
-Click on the "Import projects from Gitlab.com" link and you will be redirected to GitLab.com
+Click on the "Import projects from GitLab.com" link and you will be redirected to GitLab.com
for permission to access your projects. After accepting, you'll be automatically redirected to the importer.
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 485db4834e9..b355a91b5a6 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -1,17 +1,78 @@
# Migrating from SVN to GitLab
-SVN stands for Subversion and is a version control system (VCS).
-Git is a distributed version control system.
+Subversion (SVN) is a central version control system (VCS) while
+Git is a distributed version control system. There are some major differences
+between the two, for more information consult your favorite search engine.
-There are some major differences between the two, for more information consult your favorite search engine.
+If you are currently using an SVN repository, you can migrate the repository
+to Git and GitLab. We recommend a hard cut over - run the migration command once
+and then have all developers start using the new GitLab repository immediately.
+Otherwise, it's hard to keep changing in sync in both directions. The conversion
+process should be run on a local workstation.
-Git has tools for migrating SVN repositories to git, namely `git svn`. You can read more about this at
-[git documentation pages](http://git-scm.com/book/en/Git-and-Other-Systems-Git-and-Subversion).
+Install `svn2git`. On all systems you can install as a Ruby gem if you already
+have Ruby and Git installed.
-Apart from the [official git documentation](http://git-scm.com/book/en/Git-and-Other-Systems-Migrating-to-Git) there is also
-user created step by step guide for migrating from SVN to GitLab.
+```bash
+sudo gem install svn2git
+```
-[Benjamin New](https://github.com/leftclickben) wrote [a guide that shows how to do a migration](https://gist.github.com/leftclickben/322b7a3042cbe97ed2af). Mirrors can be found [here](https://gitlab.com/snippets/2168) and [here](https://gist.github.com/maxlazio/f1b593b0d00aa966e9ca).
+On Debian-based Linux distributions you can install the native packages:
+
+```bash
+sudo apt-get install git-core git-svn ruby
+```
+
+Optionally, prepare an authors file so `svn2git` can map SVN authors to Git authors.
+If you choose not to create the authors file then commits will not be attributed
+to the correct GitLab user. Some users may not consider this a big issue while
+others will want to ensure they complete this step. If you choose to map authors
+you will be required to map every author that is present on changes in the SVN
+repository. If you don't, the conversion will fail and you will have to update
+the author file accordingly. The following command will search through the
+repository and output a list of authors.
+
+```bash
+svn log --quiet | grep -E "r[0-9]+ \| .+ \|" | cut -d'|' -f2 | sed 's/ //g' | sort | uniq
+```
+
+Use the output from the last command to construct the authors file.
+Create a file called `authors.txt` and add one mapping per line.
+
+```
+janedoe = Jane Doe <janedoe@example.com>
+johndoe = John Doe <johndoe@example.com>
+```
+
+If your SVN repository is in the standard format (trunk, branches, tags,
+not nested) the conversion is simple. For a non-standard repository see
+[svn2git documentation](https://github.com/nirvdrum/svn2git). The following
+command will checkout the repository and do the conversion in the current
+working directory. Be sure to create a new directory for each repository before
+running the `svn2git` command. The conversion process will take some time.
+
+```bash
+svn2git https://svn.example.com/path/to/repo --authors /path/to/authors.txt
+```
+
+If your SVN repository requires a username and password add the
+`--username <username>` and `--password <password` flags to the above command.
+`svn2git` also supports excluding certain file paths, branches, tags, etc. See
+[svn2git documentation](https://github.com/nirvdrum/svn2git) or run
+`svn2git --help` for full documentation on all of the available options.
+
+Create a new GitLab project, where you will eventually push your converted code.
+Copy the SSH or HTTP(S) repository URL from the project page. Add the GitLab
+repository as a Git remote and push all the changes. This will push all commits,
+branches and tags.
+
+```bash
+git remote add origin git@gitlab.com:<group>/<project>.git
+git push --all origin
+```
## Contribute to this guide
-We welcome all contributions that would expand this guide with instructions on how to migrate from SVN and other version control systems.
+We welcome all contributions that would expand this guide with instructions on
+how to migrate from SVN and other version control systems.
+
+
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
new file mode 100644
index 00000000000..5076b2697a3
--- /dev/null
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -0,0 +1,41 @@
+# GitLab Git LFS Administration
+
+Documentation on how to use Git LFS are under [Managing large binary files with Git LFS doc](manage_large_binaries_with_git_lfs.md).
+
+## Requirements
+
+* Git LFS is supported in GitLab starting with version 8.2.
+* Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up.
+
+## Configuration
+
+Git LFS objects can be large in size. By default, they are stored on the server GitLab is installed on.
+
+There are two configuration options to help GitLab server administrators:
+
+* Enabling/disabling Git LFS support
+* Changing the location of LFS object storage
+
+### Omnibus packages
+
+In `/etc/gitlab/gitlab.rb`:
+
+```ruby
+gitlab_rails['lfs_enabled'] = false
+gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
+```
+
+### Installations from source
+
+In `config/gitlab.yml`:
+
+```yaml
+ lfs:
+ enabled: false
+ storage_path: /mnt/storage/lfs-objects
+```
+
+## Known limitations
+
+* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets) is not supported
+* Currently, removing LFS objects from GitLab Git LFS storage is not supported
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
new file mode 100644
index 00000000000..b59e92cb317
--- /dev/null
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -0,0 +1,126 @@
+# Git LFS
+
+Managing large files such as audio, video and graphics files has always been one of the shortcomings of Git.
+The general recommendation is to not have Git repositories larger than 1GB to preserve performance.
+
+GitLab already supports [managing large files with git annex](http://doc.gitlab.com/ee/workflow/git_annex.html) (EE only), however in certain
+environments it is not always convenient to use different commands to differentiate between the large files and regular ones.
+
+Git LFS makes this simpler for the end user by removing the requirement to learn new commands.
+
+## How it works
+
+Git LFS client talks with the GitLab server over HTTPS. It uses HTTP Basic Authentication to authorize client requests.
+Once the request is authorized, Git LFS client receives instructions from where to fetch or where to push the large file.
+
+## GitLab server configuration
+
+Documentation for GitLab instance administrators is under [LFS administration doc](lfs_administration.md).
+
+## Requirements
+
+* Git LFS is supported in GitLab starting with version 8.2
+* [Git LFS client](https://git-lfs.github.com) version 1.0.1 and up
+
+## Known limitations
+
+* Git LFS v1 original API is not supported since it was deprecated early in LFS development
+* When SSH is set as a remote, Git LFS objects still go through HTTPS
+* Any Git LFS request will ask for HTTPS credentials to be provided so good Git credentials store is recommended
+* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have to add the URL to Git config manually (see #troubleshooting)
+
+## Using Git LFS
+
+Lets take a look at the workflow when you need to check large files into your Git repository with Git LFS:
+For example, if you want to upload a very large file and check it into your Git repository:
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+git lfs init # initialize the Git LFS project project
+git lfs track "*.iso" # select the file extensions that you want to treat as large files
+```
+
+Once a certain file extension is marked for tracking as a LFS object you can use Git as usual without having to redo the command to track a file with the same extension:
+
+```bash
+cp ~/tmp/debian.iso ./ # copy a large file into the current directory
+git add . # add the large file to the project
+git commit -am "Added Debian iso" # commit the file meta data
+git push origin master # sync the git repo and large file to the GitLab server
+```
+
+Cloning the repository works the same as before. Git automatically detects the LFS-tracked files and clones them via HTTP. If you performed the git clone command with a SSH URL, you have to enter your GitLab credentials for HTTP authentication.
+
+```bash
+git clone git@gitlab.example.com:group/project.git
+```
+
+If you already cloned the repository and you want to get the latest LFS object that are on the remote repository, eg. from branch `master`:
+
+```bash
+git lfs fetch master
+```
+
+## Troubleshooting
+
+### error: Repository or object not found
+
+There are a couple of reasons why this error can occur:
+
+* You don't have permissions to access certain LFS object
+
+Check if you have permissions to push to the project or fetch from the project.
+
+* Project is not allowed to access the LFS object
+
+LFS object you are trying to push to the project or fetch from the project is not available to the project anymore.
+Probably the object was removed from the server.
+
+* Local git repository is using deprecated LFS API
+
+### Invalid status for <url> : 501
+
+Git LFS will log the failures into a log file.
+To view this log file, while in project directory:
+
+```bash
+git lfs logs last
+```
+
+If the status `error 501` is shown, it is because:
+
+* Git LFS support is not enabled on the GitLab server. Check with your GitLab administrator why Git LFS is not enabled on the server. See [LFS administration documentation](lfs_administration.md) for instructions on how to enable LFS support.
+
+* Git LFS client version is not supported by GitLab server. Check your Git LFS version with `git lfs version`. Check the Git config of the project for traces of deprecated API with `git lfs -l`. If `batch = false` is set in the config, remove the line and try to update your Git LFS client. Only version 1.0.1 and newer are supported.
+
+### getsockopt: connection refused
+
+If you push a LFS object to a project and you receive an error similar to: `Post <URL>/info/lfs/objects/batch: dial tcp IP: getsockopt: connection refused`,
+the LFS client is trying to reach GitLab through HTTPS. However, your GitLab instance is being served on HTTP.
+
+This behaviour is caused by Git LFS using HTTPS connections by default when a `lfsurl` is not set in the Git config.
+
+To prevent this from happening, set the lfs url in project Git config:
+
+```bash
+
+git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs/objects/batch"
+```
+
+### Credentials are always required when pushing an object
+
+Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing the LFS object on every push for every object, user HTTPS credentials are required.
+
+By default, Git has support for remembering the credentials for each repository you use. This is described in [Git credentials man pages](https://git-scm.com/docs/gitcredentials).
+
+For example, you can tell Git to remember the password for a period of time in which you expect to push the objects:
+
+```bash
+git config --global credential.helper 'cache --timeout=3600'
+```
+
+This will remember the credentials for an hour after which Git operations will require re-authentication.
+
+If you are using OS X you can use `osxkeychain` to store and encrypt your credentials. For Windows, you can use `wincred` or Microsoft's [Git Credential Manager for Windows](https://github.com/Microsoft/Git-Credential-Manager-for-Windows/releases).
+
+More details about various methods of storing the user credentials can be found on [Git Credential Storage documentation](https://git-scm.com/book/en/v2/Git-Tools-Credential-Storage). \ No newline at end of file
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 751e19da7f1..6d57b5d98cd 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -38,3 +38,15 @@ To check out a particular merge request:
```
$ git checkout origin/merge-requests/1
```
+
+## Ignore whitespace changes in Merge Request diff view
+
+![MR diff](merge_requests/merge_request_diff.png)
+
+It you add `w=1` option to URL, you can see diff without whitespace changes.
+
+![MR diff without whitespace](merge_requests/merge_request_diff_without_whitespace.png)
+
+It is also working on commits compare view.
+
+![Commit Compare](merge_requests/commit_compare.png)
diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/workflow/merge_requests/commit_compare.png
new file mode 100644
index 00000000000..46b3a56a59b
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..ed08ae91bec
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..67d67a64d12
--- /dev/null
+++ b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md
new file mode 100644
index 00000000000..75e1fdff2b2
--- /dev/null
+++ b/doc/workflow/merge_when_build_succeeds.md
@@ -0,0 +1,15 @@
+# Merge When Build Succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually.
+
+![Enable](merge_when_build_succeeds/enable.png)
+
+When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button.
+
+Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all.
+
+![Status](merge_when_build_succeeds/status.png)
+
+When the build succeeds, the merge request will automatically be merged. When the build fails, the author gets a chance to retry any failed builds, or to push new commits to fix the failure.
+
+When the builds are retried and succeed on the second try, the merge request will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed.
diff --git a/doc/workflow/merge_when_build_succeeds/enable.png b/doc/workflow/merge_when_build_succeeds/enable.png
new file mode 100644
index 00000000000..633efa1246f
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..c856c7d14dc
--- /dev/null
+++ b/doc/workflow/merge_when_build_succeeds/status.png
Binary files differ
diff --git a/doc/workflow/milestones.md b/doc/workflow/milestones.md
new file mode 100644
index 00000000000..dff36899aec
--- /dev/null
+++ b/doc/workflow/milestones.md
@@ -0,0 +1,13 @@
+# Milestones
+
+Milestones allow you to organize issues and merge requests into a cohesive group, optionally setting a due date.
+A common use is keeping track of an upcoming software version. Milestones are created per-project.
+
+![milestone form](milestones/form.png)
+
+## Groups and milestones
+
+You can create a milestone for several projects in the same group simultaneously.
+On the group's milestones page, you will be able to see the status of that milestone across all of the selected projects.
+
+![group milestone form](milestones/group_form.png)
diff --git a/doc/workflow/milestones/form.png b/doc/workflow/milestones/form.png
new file mode 100644
index 00000000000..de44c1ffc1a
--- /dev/null
+++ 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
new file mode 100644
index 00000000000..38862dcca68
--- /dev/null
+++ b/doc/workflow/milestones/group_form.png
Binary files differ
diff --git a/doc/workflow/releases.md b/doc/workflow/releases.md
new file mode 100644
index 00000000000..6176784fc57
--- /dev/null
+++ b/doc/workflow/releases.md
@@ -0,0 +1,20 @@
+# Releases
+
+You can turn any git tag into a release, by adding a note to it.
+Release notes behave like any other markdown form in GitLab so you can write text and drag-n-drop files to it.
+Release notes are stored in the database of GitLab.
+
+There are several ways to add release notes:
+
+* In the interface, when you create a new git tag with GitLab
+* In the interface, by adding a note to an existing git tag
+* with the GitLab API
+
+## New tag page with release notes text area
+
+![new_tag](releases/new_tag.png)
+
+## Tags page with button to add or edit release notes for existing git tag
+
+![tags](releases/tags.png)
+
diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png
new file mode 100644
index 00000000000..e2b64bfe17f
--- /dev/null
+++ b/doc/workflow/releases/new_tag.png
Binary files differ
diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png
new file mode 100644
index 00000000000..aca91906c68
--- /dev/null
+++ b/doc/workflow/releases/tags.png
Binary files differ
diff --git a/doc/workflow/voting_slider.png b/doc/workflow/voting_slider.png
deleted file mode 100644
index 4c660ef9593..00000000000
--- a/doc/workflow/voting_slider.png
+++ /dev/null
Binary files differ
diff --git a/doc_styleguide.md b/doc_styleguide.md
index 656bb1d17ff..cceb449a854 100644
--- a/doc_styleguide.md
+++ b/doc_styleguide.md
@@ -15,6 +15,8 @@ For subtitles, use '##', '###' and so on.
- Do not duplicate information.
- Be brief and clear.
- Whenever it applies, add documents in alphabetical order.
+- Write in US English
+- Use [single spaces](http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html) instead of double spaces.
## Images
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index 973918086a3..2edb3964f70 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -33,3 +33,19 @@ Feature: Admin Groups
When I visit admin group page
When I select user "johndoe@gitlab.com" from user list as "Reporter"
Then I should see "johndoe@gitlab.com" in team list in every project as "Reporter"
+
+ @javascript
+ Scenario: Signed in admin should be able to add himself to a group
+ Given "John Doe" is owner of group "Owned"
+ When I visit group "Owned" members page
+ When I select current user as "Developer"
+ Then I should see current user as "Developer"
+
+ @javascript
+ Scenario: Signed in admin should be able to remove himself from group
+ Given current user is developer of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see current user as "Developer"
+ When I click on the "Remove User From Group" button for current user
+ When I visit group "Owned" members page
+ Then I should not see current user as "Developer"
diff --git a/features/admin/projects.feature b/features/admin/projects.feature
index f7cec04eb75..c5ee80136c8 100644
--- a/features/admin/projects.feature
+++ b/features/admin/projects.feature
@@ -27,3 +27,19 @@ Feature: Admin Projects
And I visit admin project page
When I transfer project to group 'Web'
Then I should see project transfered
+
+ @javascript
+ Scenario: Signed in admin should be able to add himself to a project
+ Given "John Doe" owns private project "Enterprise"
+ When I visit project "Enterprise" members page
+ When I select current user as "Developer"
+ Then I should see current user as "Developer"
+
+ @javascript
+ Scenario: Signed in admin should be able to remove himself from a project
+ Given "John Doe" owns private project "Enterprise"
+ And current user is developer of project "Enterprise"
+ When I visit project "Enterprise" members page
+ Then I should see current user as "Developer"
+ When I click on the "Remove User From Project" button for current user
+ Then I should not see current user as "Developer"
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index a42e59c98f2..5fc9b135601 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -105,15 +105,6 @@ Feature: Explore Groups
When I visit the public groups area
Then I should see group "TestGroup"
- Scenario: I should not see group with internal project in public groups area
- Given group "TestGroup" has internal project "Internal"
- When I visit the public groups area
- Then I should not see group "TestGroup"
-
- Scenario: I should not see group with private project in public groups area
- When I visit the public groups area
- Then I should not see group "TestGroup"
-
Scenario: I should see group with public project in public groups area as user
Given group "TestGroup" has public project "Community"
When I sign in as a user
@@ -125,9 +116,3 @@ Feature: Explore Groups
When I sign in as a user
And I visit the public groups area
Then I should see group "TestGroup"
-
- Scenario: I should not see group with private project in public groups area as user
- When I sign in as a user
- And I visit the public groups area
- Then I should not see group "TestGroup"
-
diff --git a/features/explore/projects.feature b/features/explore/projects.feature
index 5d3870827f5..629859e960d 100644
--- a/features/explore/projects.feature
+++ b/features/explore/projects.feature
@@ -31,8 +31,17 @@ Feature: Explore Projects
Then I should see empty public project details
And I should see empty public project details with http clone info
- Scenario: I visit an empty public project page as user
+ Scenario: I visit an empty public project page as user with no ssh-keys
Given I sign in as a user
+ And I have no ssh keys
+ And public empty project "Empty Public Project"
+ When I visit empty project page
+ Then I should see empty public project details
+ And I should see empty public project details with http clone info
+
+ Scenario: I visit an empty public project page as user with an ssh-key
+ Given I sign in as a user
+ And I have an ssh key
And public empty project "Empty Public Project"
When I visit empty project page
Then I should see empty public project details
@@ -57,8 +66,16 @@ Feature: Explore Projects
Then I should see project "Community" home page
And I should see an http link to the repository
- Scenario: I visit public project page as user
+ Scenario: I visit public project page as user with no ssh-keys
+ Given I sign in as a user
+ And I have no ssh keys
+ When I visit project "Community" page
+ Then I should see project "Community" home page
+ And I should see an http link to the repository
+
+ Scenario: I visit public project page as user with an ssh-key
Given I sign in as a user
+ And I have an ssh key
When I visit project "Community" page
Then I should see project "Community" home page
And I should see an ssh link to the repository
diff --git a/features/group/members.feature b/features/group/members.feature
new file mode 100644
index 00000000000..1f9514bac39
--- /dev/null
+++ b/features/group/members.feature
@@ -0,0 +1,105 @@
+Feature: Group Members
+ Background:
+ Given I sign in as "John Doe"
+ And "John Doe" is owner of group "Owned"
+ And "John Doe" is guest of group "Guest"
+
+ @javascript
+ Scenario: I should add user to group "Owned"
+ Given User "Mary Jane" exists
+ When I visit group "Owned" members page
+ And I select user "Mary Jane" from list with role "Reporter"
+ Then I should see user "Mary Jane" in team list
+
+ @javascript
+ Scenario: Add user to group
+ Given gitlab user "Mike"
+ When I visit group "Owned" members page
+ When I select "Mike" as "Reporter"
+ Then I should see "Mike" in team list as "Reporter"
+
+ @javascript
+ Scenario: Ignore add user to group when is already Owner
+ Given gitlab user "Mike"
+ When I visit group "Owned" members page
+ When I select "Mike" as "Reporter"
+ Then I should see "Mike" in team list as "Owner"
+
+ @javascript
+ Scenario: Invite user to group
+ When I visit group "Owned" members page
+ When I select "sjobs@apple.com" as "Reporter"
+ Then I should see "sjobs@apple.com" in team list as invited "Reporter"
+
+ @javascript
+ Scenario: Edit group member permissions
+ Given "Mary Jane" is guest of group "Owned"
+ And I visit group "Owned" members page
+ When I change the "Mary Jane" role to "Developer"
+ Then I should see "Mary Jane" as "Developer"
+
+ # Leave
+
+ @javascript
+ Scenario: Owner should be able to remove himself from group if he is not the last owner
+ Given "Mary Jane" is owner of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ When I click on the "Remove User From Group" button for "John Doe"
+ And I visit group "Owned" members page
+ Then I should not see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+
+ @javascript
+ Scenario: Owner should not be able to remove himself from group if he is the last owner
+ Given "Mary Jane" is guest of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ Then I should not see the "Remove User From Group" button for "John Doe"
+
+ @javascript
+ Scenario: Guest should be able to remove himself from group
+ Given "Mary Jane" is guest of group "Guest"
+ When I visit group "Guest" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ When I click on the "Remove User From Group" button for "John Doe"
+ When I visit group "Guest" members page
+ Then I should not see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+
+ @javascript
+ Scenario: Guest should be able to remove himself from group even if he is the only user in the group
+ When I visit group "Guest" members page
+ Then I should see user "John Doe" in team list
+ When I click on the "Remove User From Group" button for "John Doe"
+ When I visit group "Guest" members page
+ Then I should not see user "John Doe" in team list
+
+ # Remove others
+
+ Scenario: Owner should be able to remove other users from group
+ Given "Mary Jane" is owner of group "Owned"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ When I click on the "Remove User From Group" button for "Mary Jane"
+ When I visit group "Owned" members page
+ Then I should see user "John Doe" in team list
+ Then I should not see user "Mary Jane" in team list
+
+ Scenario: Guest should not be able to remove other users from group
+ Given "Mary Jane" is guest of group "Guest"
+ When I visit group "Guest" members page
+ Then I should see user "John Doe" in team list
+ Then I should see user "Mary Jane" in team list
+ Then I should not see the "Remove User From Group" button for "Mary Jane"
+
+ Scenario: Search member by name
+ Given "Mary Jane" is guest of group "Guest"
+ And I visit group "Guest" members page
+ When I search for 'Mary' member
+ Then I should see user "Mary Jane" in team list
+ Then I should not see user "John Doe" in team list
diff --git a/features/group/milestones.feature b/features/group/milestones.feature
new file mode 100644
index 00000000000..62ea66a783c
--- /dev/null
+++ b/features/group/milestones.feature
@@ -0,0 +1,30 @@
+Feature: Group Milestones
+ Background:
+ Given I sign in as "John Doe"
+ And "John Doe" is owner of group "Owned"
+
+ Scenario: I should see group "Owned" milestone index page with no milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ Then I should see group milestones index page has no milestones
+
+ Scenario: I should see group "Owned" milestone index page with milestones
+ Given Group has projects with milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ Then I should see group milestones index page with milestones
+
+ Scenario: I should see group "Owned" milestone show page
+ Given Group has projects with milestones
+ When I visit group "Owned" page
+ And I click on group milestones
+ And I click on one group milestone
+ Then I should see group milestone with descriptions and expiry date
+ And I should see group milestone with all issues and MRs assigned to that milestone
+
+ Scenario: Create multiple milestones with one form
+ Given I visit group "Owned" milestones page
+ And I click new milestone button
+ And I fill milestone name
+ When I press create mileston button
+ Then milestone in each project should be created
diff --git a/features/groups.feature b/features/groups.feature
index db37fa3b375..c803e952980 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -2,7 +2,6 @@ Feature: Groups
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
- And "John Doe" is guest of group "Guest"
Scenario: I should have back to group button
When I visit group "Owned" page
@@ -24,13 +23,6 @@ Feature: Groups
When I visit group "Owned" merge requests page
Then I should see merge requests from group "Owned" assigned to me
- @javascript
- Scenario: I should add user to projects in group "Owned"
- Given User "Mary Jane" exists
- When I visit group "Owned" members page
- And I select user "Mary Jane" from list with role "Reporter"
- Then I should see user "Mary Jane" in team list
-
Scenario: I should see edit group "Owned" page
When I visit group "Owned" settings page
And I change group "Owned" name to "new-name"
@@ -51,108 +43,6 @@ Feature: Groups
Then I should not see group "Owned" avatar
And I should not see the "Remove avatar" button
- @javascript
- Scenario: Add user to group
- Given gitlab user "Mike"
- When I visit group "Owned" members page
- And I click link "Add members"
- When I select "Mike" as "Reporter"
- Then I should see "Mike" in team list as "Reporter"
-
- @javascript
- Scenario: Invite user to group
- When I visit group "Owned" members page
- And I click link "Add members"
- When I select "sjobs@apple.com" as "Reporter"
- Then I should see "sjobs@apple.com" in team list as invited "Reporter"
-
- # Leave
-
- @javascript
- Scenario: Owner should be able to remove himself from group if he is not the last owner
- Given "Mary Jane" is owner of group "Owned"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- When I click on the "Remove User From Group" button for "John Doe"
- And I visit group "Owned" members page
- Then I should not see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
-
- @javascript
- Scenario: Owner should not be able to remove himself from group if he is the last owner
- Given "Mary Jane" is guest of group "Owned"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- Then I should not see the "Remove User From Group" button for "John Doe"
-
- @javascript
- Scenario: Guest should be able to remove himself from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit group "Guest" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- When I click on the "Remove User From Group" button for "John Doe"
- When I visit group "Guest" members page
- Then I should not see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
-
- @javascript
- Scenario: Guest should be able to remove himself from group even if he is the only user in the group
- When I visit group "Guest" members page
- Then I should see user "John Doe" in team list
- When I click on the "Remove User From Group" button for "John Doe"
- When I visit group "Guest" members page
- Then I should not see user "John Doe" in team list
-
- # Remove others
-
- Scenario: Owner should be able to remove other users from group
- Given "Mary Jane" is owner of group "Owned"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- When I click on the "Remove User From Group" button for "Mary Jane"
- When I visit group "Owned" members page
- Then I should see user "John Doe" in team list
- Then I should not see user "Mary Jane" in team list
-
- Scenario: Guest should not be able to remove other users from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit group "Guest" members page
- Then I should see user "John Doe" in team list
- Then I should see user "Mary Jane" in team list
- Then I should not see the "Remove User From Group" button for "Mary Jane"
-
- Scenario: Search member by name
- Given "Mary Jane" is guest of group "Guest"
- And I visit group "Guest" members page
- When I search for 'Mary' member
- Then I should see user "Mary Jane" in team list
- Then I should not see user "John Doe" in team list
-
- # Group milestones
-
- Scenario: I should see group "Owned" milestone index page with no milestones
- When I visit group "Owned" page
- And I click on group milestones
- Then I should see group milestones index page has no milestones
-
- Scenario: I should see group "Owned" milestone index page with milestones
- Given Group has projects with milestones
- When I visit group "Owned" page
- And I click on group milestones
- Then I should see group milestones index page with milestones
-
- Scenario: I should see group "Owned" milestone show page
- Given Group has projects with milestones
- When I visit group "Owned" page
- And I click on group milestones
- And I click on one group milestone
- Then I should see group milestone with descriptions and expiry date
- And I should see group milestone with all issues and MRs assigned to that milestone
-
# Group projects in settings
Scenario: I should see all projects in the project list in settings
Given Group "Owned" has archived project
@@ -169,4 +59,4 @@ Feature: Groups
When I visit group "Owned" page
Then I should see group "Owned"
Then I should see project "Public-project"
-
+
diff --git a/features/profile/profile.feature b/features/profile/profile.feature
index 27c0bde364e..168d9d30b50 100644
--- a/features/profile/profile.feature
+++ b/features/profile/profile.feature
@@ -7,6 +7,7 @@ Feature: Profile
Given I visit profile page
Then I should see my profile info
+ @javascript
Scenario: I can see groups I belong to
Given I have group with projects
When I visit profile page
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index 8661ea98c20..2fd097d100b 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -20,11 +20,6 @@ Feature: Project Active Tab
Then the active main tab should be Commits
And no other main tabs should be active
- Scenario: On Project Network
- Given I visit my project's network page
- Then the active main tab should be Network
- And no other main tabs should be active
-
Scenario: On Project Issues
Given I visit my project's issues page
Then the active main tab should be Issues
@@ -83,6 +78,12 @@ Feature: Project Active Tab
And no other sub tabs should be active
And the active main tab should be Commits
+ Scenario: On Project Commits/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
+
Scenario: On Project Commits/Compare
Given I visit my project's commits page
And I click the "Compare" tab
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 65d8e48b9b3..2c17d32154a 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -1,3 +1,4 @@
+@project_commits
Feature: Project Commits Branches
Background:
Given I sign in as a user
@@ -24,6 +25,7 @@ Feature: Project Commits Branches
And I click branch 'improve/awesome' delete link
Then I should not see branch 'improve/awesome'
+ @javascript
Scenario: I create a branch with invalid name
Given I visit project branches page
And I click new branch link
diff --git a/features/project/commits/comments.feature b/features/project/commits/comments.feature
index 320f008abb6..fafb54b183a 100644
--- a/features/project/commits/comments.feature
+++ b/features/project/commits/comments.feature
@@ -1,3 +1,4 @@
+@project_commits
Feature: Project Commits Comments
Background:
Given I sign in as a user
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index e4beeb59adc..5bb2d0e976b 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -1,3 +1,4 @@
+@project_commits
Feature: Project Commits
Background:
Given I sign in as a user
@@ -18,7 +19,8 @@ Feature: Project Commits
Scenario: I browse commit with ci from list
Given commit has ci status
- And I click on commit link
+ And repository contains ".gitlab-ci.yml" file
+ When I click on commit link
Then I see commit ci info
And I click status link
Then I see builds list
diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature
index 4a2b870e082..2bde4c8a99b 100644
--- a/features/project/commits/diff_comments.feature
+++ b/features/project/commits/diff_comments.feature
@@ -1,3 +1,4 @@
+@project_commits
Feature: Project Commits Diff Comments
Background:
Given I sign in as a user
@@ -14,6 +15,12 @@ Feature: Project Commits Diff Comments
Then I should see a diff comment saying "Typo, please fix"
@javascript
+ Scenario: I can add a diff comment with a single emoji
+ Given I open a diff comment form
+ And I write a diff comment like ":smile:"
+ Then I should see a diff comment with an emoji image
+
+ @javascript
Scenario: I get a temporary form for the first comment on a diff line
Given I open a diff comment form
Then I should see a temporary diff comment form
diff --git a/features/project/commits/tags.feature b/features/project/commits/tags.feature
index 02f399f7cad..a4be39b2d40 100644
--- a/features/project/commits/tags.feature
+++ b/features/project/commits/tags.feature
@@ -1,3 +1,4 @@
+@project_commits
Feature: Project Commits Tags
Background:
Given I sign in as a user
@@ -12,6 +13,12 @@ Feature: Project Commits Tags
And I submit new tag form
Then I should see new tag created
+ Scenario: I create a tag with release notes
+ Given I click new tag link
+ And I submit new tag form with release notes
+ Then I should see new tag created
+ And I should see tag release notes
+
Scenario: I create a tag with invalid name
And I click new tag link
And I submit new tag form with invalid name
@@ -27,15 +34,13 @@ Feature: Project Commits Tags
And I submit new tag form with tag that already exists
Then I should see new an error that tag already exists
- @javascript
Scenario: I delete a tag
+ Given I visit tag 'v1.1.0' page
Given I delete tag 'v1.1.0'
Then I should not see tag 'v1.1.0'
- @javascript
- Scenario: I delete all tags and see info message
- Given I delete all tags
- Then I should see tags info message
-
- # @wip
- # Scenario: I can download project by tag
+ Scenario: I add release notes to the tag
+ Given I visit tag 'v1.1.0' page
+ When I click edit tag link
+ And I fill release notes and submit form
+ Then I should see tag release notes
diff --git a/features/project/commits/user_lookup.feature b/features/project/commits/user_lookup.feature
index db51d4a6cfa..c18f4e070f3 100644
--- a/features/project/commits/user_lookup.feature
+++ b/features/project/commits/user_lookup.feature
@@ -1,3 +1,4 @@
+@project_commits
Feature: Project Commits User Lookup
Background:
Given I sign in as a user
diff --git a/features/project/create.feature b/features/project/create.feature
index e9dc4fe6b3c..27136798e36 100644
--- a/features/project/create.feature
+++ b/features/project/create.feature
@@ -1,3 +1,4 @@
+@project-create
Feature: Project Create
In order to get access to project sections
A user with ability to create a project
@@ -7,6 +8,7 @@ Feature: Project Create
Scenario: User create a project
Given I sign in as a user
When I visit new project page
+ And I have an ssh key
And fill project form with valid data
Then I should see project page
And I should see empty project instuctions
@@ -14,6 +16,7 @@ Feature: Project Create
@javascript
Scenario: Empty project instructions
Given I sign in as a user
+ And I have an ssh key
When I visit new project page
And fill project form with valid data
Then I see empty project instuctions
diff --git a/features/project/fork.feature b/features/project/fork.feature
index 22f68e5b340..37cd53ee977 100644
--- a/features/project/fork.feature
+++ b/features/project/fork.feature
@@ -14,3 +14,14 @@ Feature: Project Fork
And I click link "Fork"
When I fork to my namespace
Then I should see a "Name has already been taken" warning
+
+ Scenario: Merge request on canonical repo goes to fork merge request page
+ Given I click link "Fork"
+ And I fork to my namespace
+ Then I should see the forked project page
+ When I visit project "Shop" page
+ Then I should see "New merge request"
+ And I goto the Merge Requests page
+ Then I should see "New merge request"
+ And I click link "New merge request"
+ Then I should see the new merge request page for my namespace
diff --git a/features/project/graph.feature b/features/project/graph.feature
index 2acd65aea5f..63793d6f989 100644
--- a/features/project/graph.feature
+++ b/features/project/graph.feature
@@ -18,3 +18,8 @@ Feature: Project Graph
Given project "Shop" has CI enabled
When I visit project "Shop" CI graph page
Then page should have CI graphs
+
+ @javascript
+ Scenario: I should see project languages graphs
+ When I visit project "Shop" languages graph page
+ Then page should have languages graphs
diff --git a/features/project/issues/award_emoji.feature b/features/project/issues/award_emoji.feature
new file mode 100644
index 00000000000..9a06fdc2ee6
--- /dev/null
+++ b/features/project/issues/award_emoji.feature
@@ -0,0 +1,30 @@
+@project_issues
+Feature: Award Emoji
+ Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And project "Shop" has issue "Bugfix"
+ And I visit "Bugfix" issue page
+
+ @javascript
+ Scenario: I add and remove award in the issue
+ Given I click to emoji-picker
+ And I click to emoji in the picker
+ Then I have award added
+ And I can remove it by clicking to icon
+
+ @javascript
+ Scenario: I can see the list of emoji categories
+ Given I click to emoji-picker
+ Then I can see the activity and food categories
+
+ @javascript
+ Scenario: I can search emoji
+ Given I click to emoji-picker
+ And I search "hand"
+ Then I see search result for "hand"
+
+ @javascript
+ Scenario: I add award emoji using regular comment
+ Given I leave comment with a single emoji
+ Then I have award added
diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature
index e316f519861..e07f8053fb7 100644
--- a/features/project/issues/filter_labels.feature
+++ b/features/project/issues/filter_labels.feature
@@ -1,3 +1,4 @@
+@project_issues
Feature: Project Issues Filter Labels
Background:
Given I sign in as a user
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 28cc43ef710..ab234bc7507 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -1,3 +1,4 @@
+@project_issues
Feature: Project Issues
Background:
Given I sign in as a user
@@ -197,3 +198,8 @@ Feature: Project Issues
And I submit new issue "500 error on profile"
Then I should see issue "500 error on profile"
+ @javascript
+ Scenario: Another user adds a comment to issue I'm currently viewing
+ Given I visit issue page "Release 0.4"
+ And another user adds a comment with text "Yay!" to issue "Release 0.4"
+ Then I should see a new comment with text "Yay!"
diff --git a/features/project/issues/labels.feature b/features/project/issues/labels.feature
index 039a7d83cb1..45de57f18e3 100644
--- a/features/project/issues/labels.feature
+++ b/features/project/issues/labels.feature
@@ -1,3 +1,4 @@
+@project_issues
Feature: Project Issues Labels
Background:
Given I sign in as a user
diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature
index c1a20e9b488..1af05b3c326 100644
--- a/features/project/issues/milestones.feature
+++ b/features/project/issues/milestones.feature
@@ -1,3 +1,4 @@
+@project_issues
Feature: Project Issues Milestones
Background:
Given I sign in as a user
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 83055188bac..aa9078b878f 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -1,3 +1,4 @@
+@project_merge_requests
Feature: Project Merge Requests
Background:
Given I sign in as a user
@@ -10,6 +11,21 @@ Feature: Project Merge Requests
Then I should see "Bug NS-04" in merge requests
And I should not see "Feature NS-03" in merge requests
+ Scenario: I should see CI status for merge requests
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ Given "Bug NS-05" has CI status
+ When I visit project "Shop" merge requests page
+ Then I should see merge request "Bug NS-05" with CI status
+
+ Scenario: I should not see target branch name when it is project's default branch
+ Then I should see "Bug NS-04" in merge requests
+ And I should not see "master" branch
+
+ Scenario: I should see target branch when it is different from default
+ Given project "Shop" have "Bug NS-06" open merge request
+ When I visit project "Shop" merge requests page
+ Then I should see "other_branch" branch
+
Scenario: I should see rejected merge requests
Given I click link "Closed"
Then I should see "Feature NS-03" in merge requests
@@ -69,6 +85,26 @@ Feature: Project Merge Requests
Then I should see a discussion has started on diff
@javascript
+ Scenario: I edit a comment on a merge request diff
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And I visit merge request page "Bug NS-05"
+ And I click on the Changes tab
+ And I leave a comment like "Line is wrong" on diff
+ And I change the comment "Line is wrong" to "Typo, please fix" on diff
+ Then I should not see a diff comment saying "Line is wrong"
+ And I should see a diff comment saying "Typo, please fix"
+
+ @javascript
+ Scenario: I delete a comment on a merge request diff
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And I visit merge request page "Bug NS-05"
+ And I click on the Changes tab
+ And I leave a comment like "Line is wrong" on diff
+ And I delete the comment "Line is wrong" on diff
+ And I click on the Discussion tab
+ Then I should not see any discussion
+
+ @javascript
Scenario: I comment on a line of a commit in merge request
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
diff --git a/features/project/merge_requests/accept.feature b/features/project/merge_requests/accept.feature
new file mode 100644
index 00000000000..330ec8ae0fe
--- /dev/null
+++ b/features/project/merge_requests/accept.feature
@@ -0,0 +1,28 @@
+@project_merge_requests
+Feature: Project Merge Requests Acceptance
+ Background:
+ Given There is an open Merge Request
+ And I am signed in as a developer of the project
+
+ @javascript
+ Scenario: Accepting the Merge Request and removing the source branch
+ Given I am on the Merge Request detail page
+ When I click on "Remove source branch" option
+ And I click on Accept Merge Request
+ Then I should see merge request merged
+ And I should not see the Remove Source Branch button
+
+ @javascript
+ Scenario: Accepting the Merge Request when URL has an anchor
+ Given I am on the Merge Request detail with note anchor page
+ When I click on "Remove source branch" option
+ And I click on Accept Merge Request
+ Then I should see merge request merged
+ And I should not see the Remove Source Branch button
+
+ @javascript
+ Scenario: Accepting the Merge Request without removing the source branch
+ Given I am on the Merge Request detail page
+ When I click on Accept Merge Request
+ Then I should see merge request merged
+ And I should see the Remove Source Branch button
diff --git a/features/project/service.feature b/features/project/service.feature
index 5014b52b9f6..3a7b8308524 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -7,12 +7,6 @@ Feature: Project Services
When I visit project "Shop" services page
Then I should see list of available services
- Scenario: Activate gitlab-ci service
- When I visit project "Shop" services page
- And I click gitlab-ci service link
- And I fill gitlab-ci settings
- Then I should see service settings saved
-
Scenario: Activate hipchat service
When I visit project "Shop" services page
And I click hipchat service link
@@ -61,6 +55,12 @@ Feature: Project Services
And I fill email on push settings
Then I should see email on push service settings saved
+ Scenario: Activate JIRA service
+ When I visit project "Shop" services page
+ And I click jira service link
+ And I fill jira settings
+ Then I should see jira service settings saved
+
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
And I click Irker service link
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
index 0f71c32380b..10e7c234610 100644
--- a/features/project/shortcuts.feature
+++ b/features/project/shortcuts.feature
@@ -19,7 +19,8 @@ Feature: Project Shortcuts
@javascript
Scenario: Navigate to network tab
Given I press "g" and "n"
- Then the active main tab should be Network
+ Then the active sub tab should be Network
+ And the active main tab should be Commits
@javascript
Scenario: Navigate to graphs tab
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
index 77e42a1a38b..270557cbde7 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -30,5 +30,5 @@ Feature: Project Snippets
Scenario: I destroy "Snippet one"
Given I visit snippet page "Snippet one"
- And I click link "Remove Snippet"
+ And I click link "Delete"
Then I should not see "Snippet one" in snippets
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index 6b0484b6a38..a8c276b949e 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -24,6 +24,12 @@ Feature: Project Source Browse Files
Given I click on "New file" link in repo
Then I can see new file page
+ Scenario: I can create file when I don't have write access
+ Given I don't have write access
+ And I click on "New file" link in repo
+ Then I should see a notice about a new fork having been created
+ Then I can see new file page
+
@javascript
Scenario: I can create and commit file
Given I click on "New file" link in repo
@@ -35,6 +41,39 @@ Feature: Project Source Browse Files
And I should see its new content
@javascript
+ Scenario: I can create and commit file when I don't have write access
+ Given I don't have write access
+ And I click on "New file" link in repo
+ And I edit code
+ And I fill the new file name
+ And I fill the commit message
+ And I click on "Commit Changes"
+ Then I am redirected to the fork's new merge request page
+ And I can see the new commit message
+
+ @javascript
+ Scenario: I can create and commit file with new lines at the end of file
+ Given I click on "New file" link in repo
+ And I edit code with new lines at end of file
+ And I fill the new file name
+ And I fill the commit message
+ And I click on "Commit Changes"
+ Then I am redirected to the new file
+ And I click button "Edit"
+ And I should see its content with new lines preserved at end of file
+
+ @javascript
+ Scenario: I can create and commit file and specify new branch
+ Given I click on "New file" link in repo
+ And I edit code
+ And I fill the new file name
+ And I fill the commit message
+ And I fill the new branch name
+ And I click on "Commit Changes"
+ Then I am redirected to the new merge request page
+ And I should see its new content
+
+ @javascript
Scenario: I can upload file and commit
Given I click on "Upload file" link in repo
And I upload a new text file
@@ -42,7 +81,20 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Upload file"
Then I can see the new text file
- And I am redirected to the uploaded file on new branch
+ And I am redirected to the new merge request page
+ And I can see the new commit message
+
+ @javascript
+ Scenario: I can upload file and commit when I don't have write access
+ Given I don't have write access
+ And I click on "Upload file" link in repo
+ Then I should see a notice about a new fork having been created
+ When I click on "Upload file" link in repo
+ And I upload a new text file
+ And I fill the upload file commit message
+ And I click on "Upload file"
+ Then I can see the new text file
+ And I am redirected to the fork's new merge request page
And I can see the new commit message
@javascript
@@ -57,15 +109,19 @@ Feature: Project Source Browse Files
And I can see the replacement commit message
@javascript
- Scenario: I can create and commit file and specify new branch
- Given I click on "New file" link in repo
- And I edit code
- And I fill the new file name
- And I fill the commit message
- And I fill the new branch name
- And I click on "Commit Changes"
- Then I am redirected to the new file on new branch
- And I should see its new content
+ Scenario: I can replace file and commit when I don't have write access
+ Given I don't have write access
+ And I click on ".gitignore" file in repo
+ And I see the ".gitignore"
+ And I click on "Replace"
+ Then I should see a notice about a new fork having been created
+ When I click on "Replace"
+ And I replace it with a text file
+ And I fill the replace file commit message
+ And I click on "Replace file"
+ Then I can see the new text file
+ And I am redirected to the fork's new merge request page
+ And I can see the replacement commit message
@javascript
Scenario: I can create file in empty repo
@@ -91,21 +147,33 @@ Feature: Project Source Browse Files
And I see a commit error message
@javascript
+ Scenario: I can create file with a directory name
+ Given I click on "New file" link in repo
+ And I fill the new file name with a new directory
+ And I edit code
+ And I fill the commit message
+ And I click on "Commit changes"
+ Then I am redirected to the new file with directory
+ And I should see its new content
+
+ @javascript
Scenario: I can edit file
Given I click on ".gitignore" file in repo
And I click button "Edit"
Then I can edit code
+ @javascript
+ Scenario: I can edit file when I don't have write access
+ Given I don't have write access
+ And I click on ".gitignore" file in repo
+ And I click button "Edit"
+ Then I should see a notice about a new fork having been created
+ And I can edit code
+
Scenario: If the file is binary the edit link is hidden
Given I visit a binary file in the repo
Then I cannot see the edit button
- Scenario: If I don't have edit permission the edit link is disabled
- Given public project "Community"
- And I visit project "Community" source page
- And I click on ".gitignore" file in repo
- Then The edit button is disabled
-
@javascript
Scenario: I can edit and commit file
Given I click on ".gitignore" file in repo
@@ -117,6 +185,17 @@ Feature: Project Source Browse Files
And I should see its new content
@javascript
+ Scenario: I can edit and commit file when I don't have write access
+ Given I don't have write access
+ And I click on ".gitignore" file in repo
+ And I click button "Edit"
+ And I edit code
+ And I fill the commit message
+ And I click on "Commit Changes"
+ Then I am redirected to the fork's new merge request page
+ And I can see the new commit message
+
+ @javascript
Scenario: I can edit and commit file to new branch
Given I click on ".gitignore" file in repo
And I click button "Edit"
@@ -124,7 +203,7 @@ Feature: Project Source Browse Files
And I fill the commit message
And I fill the new branch name
And I click on "Commit Changes"
- Then I am redirected to the ".gitignore" on new branch
+ Then I am redirected to the new merge request page
And I should see its new content
@javascript @wip
@@ -144,7 +223,18 @@ Feature: Project Source Browse Files
And I fill the commit message
And I fill the new branch name
And I click on "Create directory"
- Then I am redirected to the new directory
+ Then I am redirected to the new merge request page
+
+ @javascript
+ Scenario: I can create directory in repo when I don't have write access
+ Given I don't have write access
+ When I click on "New directory" link in repo
+ Then I should see a notice about a new fork having been created
+ When I click on "New directory" link in repo
+ And I fill the new directory name
+ And I fill the commit message
+ And I click on "Create directory"
+ Then I am redirected to the fork's new merge request page
@javascript
Scenario: I attempt to create an existing directory
@@ -164,15 +254,28 @@ Feature: Project Source Browse Files
Then I see diff
@javascript
- Scenario: I can remove file and commit
+ Scenario: I can delete file and commit
Given I click on ".gitignore" file in repo
And I see the ".gitignore"
- And I click on "Remove"
+ And I click on "Delete"
And I fill the commit message
- And I click on "Remove file"
+ And I click on "Delete file"
Then I am redirected to the files URL
And I don't see the ".gitignore"
+ @javascript
+ Scenario: I can delete file and commit when I don't have write access
+ Given I don't have write access
+ And I click on ".gitignore" file in repo
+ And I see the ".gitignore"
+ And I click on "Delete"
+ Then I should see a notice about a new fork having been created
+ When I click on "Delete"
+ And I fill the commit message
+ And I click on "Delete file"
+ Then I am redirected to the fork's new merge request page
+ And I can see the new commit message
+
Scenario: I can browse directory with Browse Dir
Given I click on files directory
And I click on History link
@@ -211,3 +314,9 @@ Feature: Project Source Browse Files
Given I switch ref to fix
And I visit the fix tree
Then I see the commit data for a directory with a leading dot
+
+ Scenario: I browse LFS object
+ Given I click on "files/lfs/lfs_object.iso" file in repo
+ Then I should see download link and object size
+ And I should not see lfs pointer details
+ And I should see buttons for allowed commands
diff --git a/features/project/star.feature b/features/project/star.feature
index a45f9c470ea..618f44fe6dc 100644
--- a/features/project/star.feature
+++ b/features/project/star.feature
@@ -1,3 +1,4 @@
+@project-stars
Feature: Project Star
Scenario: New projects have 0 stars
Given public project "Community"
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 09a7df59df6..06fb45c8bde 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -13,14 +13,12 @@ Feature: Project Team Management
@javascript
Scenario: Add user to project
- Given I click link "Add members"
- And I select "Mike" as "Reporter"
+ When I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
Scenario: Invite user to project
- Given I click link "Add members"
- And I select "sjobs@apple.com" as "Reporter"
+ When I select "sjobs@apple.com" as "Reporter"
Then I should see "sjobs@apple.com" in team list as invited "Reporter"
@javascript
diff --git a/features/snippets/snippets.feature b/features/snippets/snippets.feature
index 4f617b6bed8..e15d7c79342 100644
--- a/features/snippets/snippets.feature
+++ b/features/snippets/snippets.feature
@@ -24,7 +24,7 @@ Feature: Snippets
Scenario: I destroy "Personal snippet one"
Given I visit snippet page "Personal snippet one"
- And I click link "Destroy"
+ And I click link "Delete"
Then I should not see "Personal snippet one" in snippets
Scenario: I create new internal snippet
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index d27634858a2..43fd91d0d4c 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -1,5 +1,6 @@
class Spinach::Features::AdminGroups < Spinach::FeatureSteps
include SharedAuthentication
+ include SharedGroup
include SharedPaths
include SharedUser
include SharedActiveTab
@@ -88,6 +89,34 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
end
end
+ step 'I select current user as "Developer"' do
+ page.within ".users-group-form" do
+ select2(current_user.id, from: "#user_ids", multiple: true)
+ select "Developer", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see current user as "Developer"' do
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+ end
+
+ step 'I click on the "Remove User From Group" button for current user' do
+ find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+ # poltergeist always confirms popups.
+ end
+
+ step 'I should not see current user as "Developer"' do
+ page.within '.content-list' do
+ expect(page).not_to have_content(current_user.name)
+ expect(page).not_to have_content('Developer')
+ end
+ end
+
protected
def current_group
diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb
index b45d98658bc..55ddcc25085 100644
--- a/features/steps/admin/labels.rb
+++ b/features/steps/admin/labels.rb
@@ -17,7 +17,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I remove label \'bug\'' do
page.within "#label_#{bug_label.id}" do
- click_link 'Remove'
+ click_link 'Delete'
end
end
@@ -45,21 +45,21 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I submit new label \'support\'' do
visit new_admin_label_path
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#F95610'
+ fill_in 'Background color', with: '#F95610'
click_button 'Save'
end
step 'I submit new label \'bug\'' do
visit new_admin_label_path
fill_in 'Title', with: 'bug'
- fill_in 'Background Color', with: '#F95610'
+ fill_in 'Background color', with: '#F95610'
click_button 'Save'
end
step 'I submit new label with invalid color' do
visit new_admin_label_path
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#12'
+ fill_in 'Background color', with: '#12'
click_button 'Save'
end
@@ -71,7 +71,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I should see label color error message' do
page.within '.label-form' do
- expect(page).to have_content 'Color is invalid'
+ expect(page).to have_content 'Color must be a valid color code'
end
end
@@ -101,7 +101,7 @@ class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
step 'I change label \'bug\' to \'fix\'' do
fill_in 'Title', with: 'fix'
- fill_in 'Background Color', with: '#F15610'
+ fill_in 'Background color', with: '#F15610'
click_button 'Save'
end
diff --git a/features/steps/admin/projects.rb b/features/steps/admin/projects.rb
index 5a1cc9aa151..a7a28755a6c 100644
--- a/features/steps/admin/projects.rb
+++ b/features/steps/admin/projects.rb
@@ -3,6 +3,8 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
include SharedPaths
include SharedAdmin
include SharedProject
+ include SharedUser
+ include Select2Helper
step 'I should see all non-archived projects' do
Project.non_archived.each do |p|
@@ -56,6 +58,41 @@ class Spinach::Features::AdminProjects < Spinach::FeatureSteps
expect(page).to have_content 'Namespace: Web'
end
+ step 'I visit project "Enterprise" members page' do
+ project = Project.find_by!(name: "Enterprise")
+ visit namespace_project_project_members_path(project.namespace, project)
+ end
+
+ step 'I select current user as "Developer"' do
+ page.within ".users-project-form" do
+ select2(current_user.id, from: "#user_ids", multiple: true)
+ select "Developer", from: "access_level"
+ end
+
+ click_button "Add users to project"
+ end
+
+ step 'I should see current user as "Developer"' do
+ page.within '.content-list' do
+ expect(page).to have_content(current_user.name)
+ expect(page).to have_content('Developer')
+ end
+ end
+
+ step 'current user is developer of project "Enterprise"' do
+ project = Project.find_by!(name: "Enterprise")
+ project.team << [current_user, :developer]
+ end
+
+ step 'I click on the "Remove User From Project" button for current user' do
+ find(:css, 'li', text: current_user.name).find(:css, 'a.btn-remove').click
+ # poltergeist always confirms popups.
+ end
+
+ step 'I should not see current_user as "Developer"' do
+ expect(page).not_to have_selector(:css, '.content-list')
+ end
+
def project
@project ||= Project.first
end
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index 6acbf46eb20..037f7494a77 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -32,6 +32,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
page.check('Comments')
page.check('Issues events')
page.check('Merge Request events')
+ page.check('Build events')
click_on 'Save'
end
@@ -39,6 +40,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
fill_in 'Webhook', with: 'http://localhost'
fill_in 'Username', with: 'test_user'
fill_in 'Channel', with: '#test_channel'
+ page.check('Notify only broken builds')
end
step 'I should see service template settings saved' do
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index f0fbd8a826a..63f0ec2b6e8 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -12,7 +12,7 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
end
step 'I should see "Shop" project CI status' do
- expect(page).to have_link "Build status: skipped"
+ expect(page).to have_link "Build skipped"
end
step 'I should see last push widget' do
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index 44a4aa9844a..a0aad66184d 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -44,7 +44,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
git_import_instructions = first('.js-toggle-content')
expect(git_import_instructions).to be_visible
expect(git_import_instructions).to have_content "Git repository URL"
- expect(git_import_instructions).to have_content "The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL:"
end
step 'I click on "Google Code"' do
diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb
index 87cd33c37eb..87f32e70d59 100644
--- a/features/steps/explore/groups.rb
+++ b/features/steps/explore/groups.rb
@@ -75,18 +75,18 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
name: projectname,
path: "#{groupname}-#{projectname}",
visibility_level: visibility_level
- )
+ )
create(:issue,
title: "#{projectname} feature",
project: project
- )
+ )
create(:merge_request,
title: "#{projectname} feature implemented",
source_project: project,
target_project: project
- )
+ )
create(:closed_issue_event,
project: project
- )
+ )
end
end
diff --git a/features/steps/explore/projects.rb b/features/steps/explore/projects.rb
index 8b498e7b4a6..742ba5d71f6 100644
--- a/features/steps/explore/projects.rb
+++ b/features/steps/explore/projects.rb
@@ -2,6 +2,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
include SharedProject
+ include SharedUser
step 'I should see project "Empty Public Project"' do
expect(page).to have_content "Empty Public Project"
@@ -60,11 +61,11 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
create(:issue,
title: "Bug",
project: public_project
- )
+ )
create(:issue,
title: "New feature",
project: public_project
- )
+ )
visit namespace_project_issues_path(public_project.namespace, public_project)
end
@@ -79,11 +80,11 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
create(:issue,
title: "Internal Bug",
project: internal_project
- )
+ )
create(:issue,
title: "New internal feature",
project: internal_project
- )
+ )
visit namespace_project_issues_path(internal_project.namespace, internal_project)
end
@@ -103,7 +104,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
title: "Bug fix for public project",
source_project: public_project,
target_project: public_project,
- )
+ )
end
step 'I should see list of merge requests for "Community" project' do
@@ -120,7 +121,7 @@ class Spinach::Features::ExploreProjects < Spinach::FeatureSteps
title: "Feature implemented",
source_project: internal_project,
target_project: internal_project
- )
+ )
end
step 'I should see list of merge requests for "Internal" project' do
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
new file mode 100644
index 00000000000..0706df3aec5
--- /dev/null
+++ b/features/steps/group/members.rb
@@ -0,0 +1,147 @@
+class Spinach::Features::GroupMembers < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedGroup
+ include SharedUser
+ include Select2Helper
+
+ step 'I select "Mike" as "Reporter"' do
+ user = User.find_by(name: "Mike")
+
+ page.within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I select "Mike" as "Master"' do
+ user = User.find_by(name: "Mike")
+
+ page.within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Master", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see "Mike" in team list as "Reporter"' do
+ page.within '.content-list' do
+ expect(page).to have_content('Mike')
+ expect(page).to have_content('Reporter')
+ end
+ end
+
+ step 'I should see "Mike" in team list as "Owner"' do
+ page.within '.content-list' do
+ expect(page).to have_content('Mike')
+ expect(page).to have_content('Owner')
+ end
+ end
+
+ step 'I select "sjobs@apple.com" as "Reporter"' do
+ page.within ".users-group-form" do
+ select2("sjobs@apple.com", from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ 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('Reporter')
+ end
+ end
+
+ step 'I select user "Mary Jane" from list with role "Reporter"' do
+ user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
+
+ page.within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see user "John Doe" in team list' do
+ expect(group_members_list).to have_content("John Doe")
+ end
+
+ step 'I should not see user "John Doe" in team list' do
+ expect(group_members_list).not_to have_content("John Doe")
+ end
+
+ step 'I should see user "Mary Jane" in team list' do
+ expect(group_members_list).to have_content("Mary Jane")
+ end
+
+ step 'I should not see user "Mary Jane" in team list' do
+ expect(group_members_list).not_to have_content("Mary Jane")
+ end
+
+ step 'I click on the "Remove User From Group" button for "John Doe"' do
+ find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click
+ # poltergeist always confirms popups.
+ end
+
+ step 'I click on the "Remove User From Group" button for "Mary Jane"' do
+ find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click
+ # poltergeist always confirms popups.
+ end
+
+ step 'I should not see the "Remove User From Group" button for "John Doe"' do
+ expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
+ # poltergeist always confirms popups.
+ end
+
+ step 'I should not see the "Remove User From Group" button for "Mary Jane"' do
+ expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove')
+ # poltergeist always confirms popups.
+ end
+
+ step 'I search for \'Mary\' member' do
+ page.within '.member-search-form' do
+ fill_in 'search', with: 'Mary'
+ click_button 'Search'
+ end
+ end
+
+ step 'I change the "Mary Jane" role to "Developer"' do
+ 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
+ end
+ end
+
+ step 'I should see "Mary Jane" as "Developer"' do
+ member = mary_jane_member
+
+ page.within "#group_member_#{member.id}" do
+ page.within '.member-access-level' do
+ expect(page).to have_content "Developer"
+ end
+ end
+ end
+
+ private
+
+ def mary_jane_member
+ user = User.find_by(name: "Mary Jane")
+ owned_group.members.find_by(user_id: user.id)
+ end
+
+ def group_members_list
+ find(".panel .content-list")
+ end
+end
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
new file mode 100644
index 00000000000..6e57b16ccb6
--- /dev/null
+++ b/features/steps/group/milestones.rb
@@ -0,0 +1,90 @@
+class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedGroup
+ include SharedUser
+
+ step 'I click on group milestones' do
+ click_link 'Milestones'
+ end
+
+ step 'I should see group milestones index page has no milestones' do
+ expect(page).to have_content('No milestones to show')
+ end
+
+ step 'Group has projects with milestones' do
+ group_milestone
+ end
+
+ step 'I should see group milestones index page with milestones' do
+ expect(page).to have_content('Version 7.2')
+ expect(page).to have_content('GL-113')
+ expect(page).to have_link('3 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2"))
+ expect(page).to have_link('0 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113"))
+ end
+
+ step 'I click on one group milestone' do
+ click_link 'GL-113'
+ end
+
+ step 'I should see group milestone with descriptions and expiry date' do
+ expect(page).to have_content('expires at Aug 20, 2114')
+ end
+
+ step 'I should see group milestone with all issues and MRs assigned to that milestone' do
+ expect(page).to have_content('Milestone GL-113')
+ expect(page).to have_content('Progress: 0 closed – 3 open')
+ issue = Milestone.find_by(name: 'GL-113').issues.first
+ expect(page).to have_link(issue.title, href: namespace_project_issue_path(issue.project.namespace, issue.project, issue))
+ end
+
+ step 'I fill milestone name' do
+ fill_in 'milestone_title', with: 'v2.9.0'
+ end
+
+ step 'I click new milestone button' do
+ click_link "New Milestone"
+ end
+
+ step 'I press create mileston button' do
+ click_button "Create Milestone"
+ end
+
+ step 'milestone in each project should be created' do
+ group = Group.find_by(name: 'Owned')
+ expect(page).to have_content "Milestone v2.9.0"
+ expect(group.projects).to be_present
+
+ group.projects.each do |project|
+ expect(page).to have_content project.name
+ end
+ end
+
+ private
+
+ def group_milestone
+ group = owned_group
+
+ %w(gitlabhq gitlab-ci cookbook-gitlab).each do |path|
+ project = create :project, path: path, group: group
+ milestone = create :milestone, title: "Version 7.2", project: project
+ create :issue,
+ project: project,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone
+
+ milestone = create :milestone,
+ title: "GL-113",
+ project: project,
+ due_date: '2114-08-20',
+ description: 'Lorem Ipsum is simply dummy text'
+
+ create :issue,
+ project: project,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone
+ end
+ end
+end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 69ddfa42c06..4c5122d1b7d 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -3,18 +3,9 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include SharedPaths
include SharedGroup
include SharedUser
- include Select2Helper
step 'I should see back to dashboard button' do
- expect(page).to have_content 'Back to dashboard'
- end
-
- step 'gitlab user "Mike"' do
- create(:user, name: "Mike")
- end
-
- step 'I click link "Add members"' do
- find(:css, 'button.btn-new').click
+ expect(page).to have_content 'Go to dashboard'
end
step 'I should see group "Owned"' do
@@ -26,7 +17,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
step 'Group "Owned" has a public project "Public-project"' do
- group = Group.find_by(name: "Owned")
+ group = owned_group
@project = create :empty_project, :public,
group: group,
@@ -37,43 +28,8 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
expect(page).to have_content 'Public-project'
end
- step 'I select "Mike" as "Reporter"' do
- user = User.find_by(name: "Mike")
-
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add users to group"
- end
-
- step 'I should see "Mike" in team list as "Reporter"' do
- page.within '.well-list' do
- expect(page).to have_content('Mike')
- expect(page).to have_content('Reporter')
- end
- end
-
- step 'I select "sjobs@apple.com" as "Reporter"' do
- page.within ".users-group-form" do
- select2("sjobs@apple.com", from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
-
- click_button "Add users to group"
- end
-
- step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
- page.within '.well-list' do
- expect(page).to have_content('sjobs@apple.com')
- expect(page).to have_content('invited')
- expect(page).to have_content('Reporter')
- end
- end
-
step 'I should see group "Owned" projects list' do
- Group.find_by(name: "Owned").projects.each do |project|
+ owned_group.projects.each do |project|
expect(page).to have_link project.name
end
end
@@ -94,36 +50,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
end
end
- step 'I select user "Mary Jane" from list with role "Reporter"' do
- user = User.find_by(name: "Mary Jane") || create(:user, name: "Mary Jane")
- click_button 'Add members'
- page.within ".users-group-form" do
- select2(user.id, from: "#user_ids", multiple: true)
- select "Reporter", from: "access_level"
- end
- click_button "Add users to group"
- end
-
- step 'I should see user "John Doe" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).to have_content("John Doe")
- end
-
- step 'I should not see user "John Doe" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).not_to have_content("John Doe")
- end
-
- step 'I should see user "Mary Jane" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).to have_content("Mary Jane")
- end
-
- step 'I should not see user "Mary Jane" in team list' do
- projects_with_access = find(".panel .well-list")
- expect(projects_with_access).not_to have_content("Mary Jane")
- end
-
step 'project from group "Owned" has issues assigned to me' do
create :issue,
project: project,
@@ -154,12 +80,12 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'I change group "Owned" avatar' do
attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save group"
- Group.find_by(name: "Owned").reload
+ owned_group.reload
end
step 'I should see new group "Owned" avatar' do
- expect(Group.find_by(name: "Owned").avatar).to be_instance_of AvatarUploader
- expect(Group.find_by(name: "Owned").avatar.url).to eq "/uploads/group/avatar/#{ Group.find_by(name:"Owned").id }/banana_sample.gif"
+ 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"
end
step 'I should see the "Remove avatar" button' do
@@ -169,83 +95,22 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
step 'I have group "Owned" avatar' do
attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
click_button "Save group"
- Group.find_by(name: "Owned").reload
+ owned_group.reload
end
step 'I remove group "Owned" avatar' do
click_link "Remove avatar"
- Group.find_by(name: "Owned").reload
+ owned_group.reload
end
step 'I should not see group "Owned" avatar' do
- expect(Group.find_by(name: "Owned").avatar?).to eq false
+ expect(owned_group.avatar?).to eq false
end
step 'I should not see the "Remove avatar" button' do
expect(page).not_to have_link("Remove avatar")
end
- step 'I click on the "Remove User From Group" button for "John Doe"' do
- find(:css, 'li', text: "John Doe").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Remove User From Group" button for "Mary Jane"' do
- find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "John Doe"' do
- expect(find(:css, 'li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Remove User From Group" button for "Mary Jane"' do
- expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove')
- # poltergeist always confirms popups.
- end
-
- step 'I search for \'Mary\' member' do
- page.within '.member-search-form' do
- fill_in 'search', with: 'Mary'
- click_button 'Search'
- end
- end
-
- step 'I click on group milestones' do
- click_link 'Milestones'
- end
-
- step 'I should see group milestones index page has no milestones' do
- expect(page).to have_content('No milestones to show')
- end
-
- step 'Group has projects with milestones' do
- group_milestone
- end
-
- step 'I should see group milestones index page with milestones' do
- expect(page).to have_content('Version 7.2')
- expect(page).to have_content('GL-113')
- expect(page).to have_link('2 Issues', href: issues_group_path("owned", milestone_title: "Version 7.2"))
- expect(page).to have_link('3 Merge Requests', href: merge_requests_group_path("owned", milestone_title: "GL-113"))
- end
-
- step 'I click on one group milestone' do
- click_link 'GL-113'
- end
-
- step 'I should see group milestone with descriptions and expiry date' do
- expect(page).to have_content('expires at Aug 20, 2114')
- end
-
- step 'I should see group milestone with all issues and MRs assigned to that milestone' do
- expect(page).to have_content('Milestone GL-113')
- expect(page).to have_content('Progress: 0 closed – 4 open')
- expect(page).to have_link(@issue1.title, href: namespace_project_issue_path(@project1.namespace, @project1, @issue1))
- expect(page).to have_link(@mr3.title, href: namespace_project_merge_request_path(@project3.namespace, @project3, @mr3))
- end
-
step 'Group "Owned" has archived project' do
group = Group.find_by(name: 'Owned')
create(:project, namespace: group, archived: true, path: "archived-project")
@@ -255,79 +120,13 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
end
- protected
+ private
def assigned_to_me(key)
project.send(key).where(assignee_id: current_user.id)
end
def project
- Group.find_by(name: "Owned").projects.first
- end
-
- def group_milestone
- group = Group.find_by(name: "Owned")
-
- @project1 = create :project,
- group: group
- project2 = create :project,
- path: 'gitlab-ci',
- group: group
- @project3 = create :project,
- path: 'cookbook-gitlab',
- group: group
- milestone1_project1 = create :milestone,
- title: "Version 7.2",
- project: @project1
- milestone1_project2 = create :milestone,
- title: "Version 7.2",
- project: project2
- create :milestone,
- title: "Version 7.2",
- project: @project3
- milestone2_project1 = create :milestone,
- title: "GL-113",
- project: @project1
- milestone2_project2 = create :milestone,
- title: "GL-113",
- project: project2
- milestone2_project3 = create :milestone,
- title: "GL-113",
- project: @project3,
- due_date: '2114-08-20',
- description: 'Lorem Ipsum is simply dummy text of the printing and typesetting industry'
- @issue1 = create :issue,
- project: @project1,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project1
- create :issue,
- project: project2,
- assignee: current_user,
- author: current_user,
- milestone: milestone1_project2
- create :issue,
- project: @project3,
- assignee: current_user,
- author: current_user,
- milestone: milestone1_project1
- create :merge_request,
- source_project: @project1,
- target_project: @project1,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project1
- create :merge_request,
- source_project: project2,
- target_project: project2,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project2
- @mr3 = create :merge_request,
- source_project: @project3,
- target_project: @project3,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project3
+ owned_group.projects.first
end
end
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 8cf24705a5e..0305f7e6da0 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -34,7 +34,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I should see new avatar' do
expect(@user.avatar).to be_instance_of AvatarUploader
- expect(@user.avatar.url).to eq "/uploads/user/avatar/#{ @user.id }/banana_sample.gif"
+ expect(@user.avatar.url).to eq "/uploads/user/avatar/#{@user.id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
@@ -59,7 +59,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
step 'I should not see the "Remove avatar" button' do
expect(page).not_to have_link("Remove avatar")
end
-
+
step 'I should see the gravatar host link' do
expect(page).to have_link("gravatar.com")
end
@@ -159,10 +159,9 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I should see my user page' do
- expect(page).to have_content "User Activity"
-
- page.within '.navbar-gitlab' do
+ page.within ".cover-block" do
expect(page).to have_content current_user.name
+ expect(page).to have_content current_user.username
end
end
@@ -176,7 +175,13 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
end
step 'I should see groups I belong to' do
- expect(page).to have_css('.profile-groups-avatars', visible: true)
+ page.within ".content" do
+ click_link "Groups"
+ end
+
+ page.within "#groups" do
+ expect(page).to have_content @group.name
+ end
end
step 'I click on new application button' do
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 338f5e8d3ee..0a42931147d 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -61,7 +61,8 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
end
step 'I should see new an error that branch is invalid' do
- expect(page).to have_content 'Branch name invalid'
+ expect(page).to have_content 'Branch name is invalid'
+ expect(page).to have_content "can't contain spaces"
end
step 'I should see new an error that ref is invalid' do
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index e5b3f27135d..a3141fe3be1 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -104,10 +104,14 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'commit has ci status' do
@project.enable_ci
- ci_commit = create :ci_commit, gl_project: @project, sha: sample_commit.id
+ ci_commit = create :ci_commit, project: @project, sha: sample_commit.id
create :ci_build, commit: ci_commit
end
+ step 'repository contains ".gitlab-ci.yml" file' do
+ allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file).and_return(String.new)
+ end
+
step 'I see commit ci info' do
expect(page).to have_content "build: pending"
end
@@ -118,6 +122,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'I see builds list' do
expect(page).to have_content "build: pending"
- expect(page).to have_content "Latest builds"
+ expect(page).to have_content "1 build"
end
end
diff --git a/features/steps/project/commits/tags.rb b/features/steps/project/commits/tags.rb
index e6f8faf50fd..eff4234a44a 100644
--- a/features/steps/project/commits/tags.rb
+++ b/features/steps/project/commits/tags.rb
@@ -18,6 +18,18 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps
click_button 'Create tag'
end
+ step 'I submit new tag form with release notes' do
+ fill_in 'tag_name', with: 'v7.0'
+ fill_in 'ref', with: 'master'
+ fill_in 'release_description', with: 'Awesome release notes'
+ click_button 'Create tag'
+ end
+
+ step 'I fill release notes and submit form' do
+ fill_in 'release_description', with: 'Awesome release notes'
+ click_button 'Save changes'
+ end
+
step 'I submit new tag form with invalid name' do
fill_in 'tag_name', with: 'v 1.0'
fill_in 'ref', with: 'master'
@@ -52,31 +64,27 @@ class Spinach::Features::ProjectCommitsTags < Spinach::FeatureSteps
expect(page).to have_content 'Tag already exists'
end
+ step "I visit tag 'v1.1.0' page" do
+ click_link 'v1.1.0'
+ end
+
step "I delete tag 'v1.1.0'" do
- page.within '.tags' do
+ page.within('.content') do
first('.btn-remove').click
- sleep 0.05
end
end
step "I should not see tag 'v1.1.0'" do
page.within '.tags' do
- expect(page.all(visible: true)).not_to have_content 'v1.1.0'
+ expect(page).not_to have_link 'v1.1.0'
end
end
- step 'I delete all tags' do
- page.within '.tags' do
- page.all('.btn-remove').each do |remove|
- remove.click
- sleep 0.05
- end
- end
+ step 'I click edit tag link' do
+ click_link 'Edit release notes'
end
- step 'I should see tags info message' do
- page.within '.tags' do
- expect(page).to have_content 'Repository has no tags yet.'
- end
+ step 'I should see tag release notes' do
+ expect(page).to have_content 'Awesome release notes'
end
end
diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb
index 0d39e1997b5..8a0e8fc2b6c 100644
--- a/features/steps/project/create.rb
+++ b/features/steps/project/create.rb
@@ -1,6 +1,7 @@
class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
+ include SharedUser
step 'fill project form with valid data' do
fill_in 'project_path', with: 'Empty'
@@ -25,7 +26,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
end
step 'I click on HTTP' do
- click_button 'HTTP'
+ find('#clone-dropdown').click
+ find('#http-selector').click
end
step 'Remote url should update to http link' do
@@ -33,7 +35,8 @@ class Spinach::Features::ProjectCreate < Spinach::FeatureSteps
end
step 'If I click on SSH' do
- click_button 'SSH'
+ find('#clone-dropdown').click
+ find('#ssh-selector').click
end
step 'Remote url should update to ssh link' do
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index b0230add34f..e98bd51ca89 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -30,4 +30,23 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
click_link current_user.name
end
end
+
+ step 'I should see "New merge request"' do
+ expect(page).to have_content(/new merge request/i)
+ end
+
+ step 'I goto the Merge Requests page' do
+ page.within '.page-sidebar-expanded' do
+ click_link "Merge Requests"
+ end
+ end
+
+ step 'I click link "New merge request"' do
+ expect(page).to have_content(/new merge request/i)
+ click_link "New Merge Request"
+ end
+
+ step 'I should see the new merge request page for my namespace' do
+ current_path.should have_content(/#{current_user.namespace.name}/i)
+ end
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 2a333222fb2..cbdce78dc0c 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -39,14 +39,15 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
select "fix", from: "merge_request_source_branch"
select "master", from: "merge_request_target_branch"
- click_button "Compare branches"
+ click_button "Compare branches and continue"
+
+ expect(page).to have_css("h3.page-title", text: "New Merge Request")
- expect(page).to have_content "New merge request"
fill_in "merge_request_title", with: "Merge Request On Forked Project"
end
step 'I submit the merge request' do
- click_button "Submit new merge request"
+ click_button "Submit merge request"
end
step 'I follow the target commit link' do
@@ -112,11 +113,10 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I fill out an invalid "Merge Request On Forked Project" merge request' do
- select "Select branch", from: "merge_request_target_branch"
expect(find(:select, "merge_request_source_project_id", {}).value).to eq @forked_project.id.to_s
expect(find(:select, "merge_request_target_project_id", {}).value).to eq @project.id.to_s
expect(find(:select, "merge_request_source_branch", {}).value).to eq ""
- expect(find(:select, "merge_request_target_branch", {}).value).to eq ""
+ expect(find(:select, "merge_request_target_branch", {}).value).to eq "master"
click_button "Compare branches"
end
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 4abd5288d51..b09ec86e5df 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -14,6 +14,15 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
visit commits_namespace_project_graph_path(project.namespace, project, "master")
end
+ step 'I visit project "Shop" languages graph page' do
+ visit languages_namespace_project_graph_path(project.namespace, project, "master")
+ end
+
+ step 'page should have languages graphs' do
+ expect(page).to have_content "Ruby 66.63 %"
+ expect(page).to have_content "JavaScript 22.96 %"
+ end
+
step 'page should have commits graphs' do
expect(page).to have_content "Commit statistics for master"
expect(page).to have_content "Commits per day of month"
@@ -25,9 +34,9 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
step 'page should have CI graphs' do
expect(page).to have_content 'Overall'
- expect(page).to have_content 'Builds chart for last week'
- expect(page).to have_content 'Builds chart for last month'
- expect(page).to have_content 'Builds chart for last year'
+ expect(page).to have_content 'Builds for last week'
+ expect(page).to have_content 'Builds for last month'
+ expect(page).to have_content 'Builds for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits'
end
diff --git a/features/steps/project/hooks.rb b/features/steps/project/hooks.rb
index df4a23a3716..be4db770948 100644
--- a/features/steps/project/hooks.rb
+++ b/features/steps/project/hooks.rb
@@ -70,8 +70,6 @@ class Spinach::Features::ProjectHooks < Spinach::FeatureSteps
step 'I should see hook service down error message' do
expect(page).to have_selector '.flash-alert',
- text: 'Hook execution failed. '\
- 'Ensure hook URL is correct and '\
- 'service is up.'
+ text: 'Hook execution failed: Exception from'
end
end
diff --git a/features/steps/project/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
new file mode 100644
index 00000000000..2c2ed08655e
--- /dev/null
+++ b/features/steps/project/issues/award_emoji.rb
@@ -0,0 +1,69 @@
+class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+ include Select2Helper
+
+ step 'I visit "Bugfix" issue page' do
+ visit namespace_project_issue_path(@project.namespace, @project, @issue)
+ end
+
+ step 'I click to emoji-picker' do
+ page.within '.awards-controls' do
+ page.find('.add-award').click
+ end
+ end
+
+ step 'I click to emoji in the picker' do
+ page.within '.emoji-menu-content' do
+ page.first('.emoji-icon').click
+ end
+ end
+
+ step 'I can remove it by clicking to icon' do
+ page.within '.awards' do
+ expect do
+ page.find('.award.active').click
+ sleep 0.3
+ end.to change{ page.all(".award").size }.from(3).to(2)
+ end
+ end
+
+ 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'
+ end
+ end
+
+ step 'I have award added' do
+ page.within '.awards' do
+ expect(page).to have_selector '.award'
+ expect(page.find('.award.active .counter')).to have_content '1'
+ end
+ end
+
+ step 'project "Shop" has issue "Bugfix"' do
+ @project = Project.find_by(name: 'Shop')
+ @issue = create(:issue, title: 'Bugfix', project: project)
+ end
+
+ step 'I leave comment with a single emoji' do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: ':smile:'
+ click_button 'Add Comment'
+ end
+ end
+
+ step 'I search "hand"' do
+ page.within('.emoji-menu-content') do
+ fill_in 'emoji_search', with: 'hand'
+ end
+ end
+
+ step 'I see search result for "hand"' do
+ page.within '.emoji-menu-content' do
+ expect(page).to have_selector '[data-emoji="raised_hand"]'
+ end
+ end
+end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index af2da41badb..8e8c9c57452 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -59,26 +59,25 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I click "author" dropdown' do
- first('.ajax-users-select').click
+ first('#s2id_author_id').click
end
step 'I see current user as the first user' do
- expect(page).to have_selector('.user-result', visible: true, count: 4)
+ expect(page).to have_selector('.user-result', visible: true, count: 3)
users = page.all('.user-name')
- expect(users[0].text).to eq 'Any'
- expect(users[1].text).to eq 'Unassigned'
- expect(users[2].text).to eq current_user.name
+ expect(users[0].text).to eq 'Any Author'
+ expect(users[1].text).to eq current_user.name
end
step 'I submit new issue "500 error on profile"' do
fill_in "issue_title", with: "500 error on profile"
- click_button "Submit new issue"
+ click_button "Submit issue"
end
step 'I submit new issue "500 error on profile" with label \'bug\'' do
fill_in "issue_title", with: "500 error on profile"
select 'bug', from: "Labels"
- click_button "Submit new issue"
+ click_button "Submit issue"
end
step 'I click link "500 error on profile"' do
@@ -86,7 +85,7 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'I should see label \'bug\' with issue' do
- page.within '.issue-show-labels' do
+ page.within '.issuable-show-labels' do
expect(page).to have_content 'bug'
end
end
@@ -284,6 +283,16 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
end
+ 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!')
+ end
+
+ step 'I should see a new comment with text "Yay!"' do
+ page.within '#notes' do
+ expect(page).to have_content('Yay!')
+ end
+ end
def filter_issue(text)
fill_in 'issue_search', with: text
end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
index d656acf4220..2ab8956867b 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 'Remove'
+ click_link 'Delete'
end
end
@@ -31,20 +31,20 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I submit new label \'support\'' do
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#F95610'
- click_button 'Save'
+ fill_in 'Background color', with: '#F95610'
+ click_button 'Create Label'
end
step 'I submit new label \'bug\'' do
fill_in 'Title', with: 'bug'
- fill_in 'Background Color', with: '#F95610'
- click_button 'Save'
+ fill_in 'Background color', with: '#F95610'
+ click_button 'Create Label'
end
step 'I submit new label with invalid color' do
fill_in 'Title', with: 'support'
- fill_in 'Background Color', with: '#12'
- click_button 'Save'
+ fill_in 'Background color', with: '#12'
+ click_button 'Create Label'
end
step 'I should see label label exist error message' do
@@ -55,7 +55,7 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I should see label color error message' do
page.within '.label-form' do
- expect(page).to have_content 'Color is invalid'
+ expect(page).to have_content 'Color must be a valid color code'
end
end
@@ -85,8 +85,8 @@ class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
step 'I change label \'bug\' to \'fix\'' do
fill_in 'Title', with: 'fix'
- fill_in 'Background Color', with: '#F15610'
- click_button 'Save'
+ fill_in 'Background color', with: '#F15610'
+ click_button 'Save changes'
end
step 'I should see label \'fix\'' do
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index c8708572ec6..e2eda511497 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -63,7 +63,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
end
step 'I click link to remove milestone' do
- click_link 'Remove'
+ click_link 'Delete'
end
step 'I should see no milestones' do
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 875bf6c4676..be993d11093 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -40,6 +40,14 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content "Bug NS-04"
end
+ step 'I should not see "master" branch' do
+ expect(page).not_to have_content "master"
+ end
+
+ step 'I should see "other_branch" branch' do
+ expect(page).to have_content "other_branch"
+ end
+
step 'I should see "Bug NS-04" in merge requests' do
expect(page).to have_content "Bug NS-04"
end
@@ -78,7 +86,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
select "feature", from: "merge_request_target_branch"
click_button "Compare branches"
fill_in "merge_request_title", with: "Wiki Feature"
- click_button "Submit new merge request"
+ click_button "Submit merge request"
end
step 'project "Shop" have "Bug NS-04" open merge request' do
@@ -93,6 +101,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
)
end
+ step 'project "Shop" have "Bug NS-06" open merge request' do
+ create(:merge_request,
+ title: "Bug NS-06",
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'other_branch',
+ author: project.users.first,
+ description: "# Description header"
+ )
+ end
+
step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do
create(:merge_request_with_diffs,
title: "Bug NS-05",
@@ -166,6 +186,50 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
leave_comment "Line is wrong"
end
+ step 'I change the comment "Line is wrong" to "Typo, please fix" on diff' do
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.js-note-edit').click
+
+ page.within('.current-note-edit-form', visible: true) do
+ fill_in 'note_note', with: 'Typo, please fix'
+ click_button 'Save Comment'
+ end
+
+ expect(page).not_to have_button 'Save Comment', disabled: true, visible: true
+ end
+ end
+
+ step 'I should not see a diff comment saying "Line is wrong"' do
+ page.within('.diff-file:nth-of-type(5) .note') do
+ expect(page).not_to have_visible_content 'Line is wrong'
+ end
+ end
+
+ step 'I should see a diff comment saying "Typo, please fix"' do
+ page.within('.diff-file:nth-of-type(5) .note') do
+ expect(page).to have_visible_content 'Typo, please fix'
+ end
+ end
+
+ step 'I delete the comment "Line is wrong" on diff' do
+ page.within('.diff-file:nth-of-type(5) .note') do
+ find('.js-note-delete').click
+ end
+ end
+
+ step 'I click on the Discussion tab' do
+ page.within '.merge-request-tabs' do
+ click_link 'Discussion'
+ end
+
+ # Waits for load
+ expect(page).to have_css('.tab-content #notes.active')
+ end
+
+ step 'I should not see any discussion' do
+ expect(page).not_to have_css('.notes .discussion')
+ end
+
step 'I should see a discussion has started on diff' do
page.within(".notes .discussion") do
page.should have_content "#{current_user.name} started a discussion"
@@ -209,7 +273,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see merged request' do
- page.within '.issue-box' do
+ page.within '.status-box' do
expect(page).to have_content "Merged"
end
end
@@ -219,7 +283,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see reopened merge request "Bug NS-04"' do
- page.within '.issue-box' do
+ page.within '.status-box' do
expect(page).to have_content "Open"
end
end
@@ -338,6 +402,19 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(page).to have_content('diff --git')
end
+ 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
+ create :ci_build, commit: ci_commit
+ 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"
+ end
+ end
+
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
diff --git a/features/steps/project/merge_requests/acceptance.rb b/features/steps/project/merge_requests/acceptance.rb
new file mode 100644
index 00000000000..2685f5fd6b4
--- /dev/null
+++ b/features/steps/project/merge_requests/acceptance.rb
@@ -0,0 +1,43 @@
+class Spinach::Features::ProjectMergeRequestsAcceptance < Spinach::FeatureSteps
+ include LoginHelpers
+ include GitlabRoutingHelper
+
+ step 'I am on the Merge Request detail page' do
+ visit merge_request_path(@merge_request)
+ end
+
+ step 'I am on the Merge Request detail with note anchor page' do
+ visit merge_request_path(@merge_request, anchor: 'note_123')
+ end
+
+ step 'I click on "Remove source branch" option' do
+ check('Remove source branch')
+ end
+
+ step 'I click on Accept Merge Request' do
+ click_button('Accept Merge Request')
+ end
+
+ step 'I should see the Remove Source Branch button' do
+ expect(page).to have_link('Remove Source Branch')
+ end
+
+ step 'I should not see the Remove Source Branch button' do
+ expect(page).not_to have_link('Remove Source Branch')
+ end
+
+ step 'There is an open Merge Request' do
+ @user = create(:user)
+ @project = create(:project, :public)
+ @project_member = create(:project_member, user: @user, project: @project, access_level: ProjectMember::DEVELOPER)
+ @merge_request = create(:merge_request, :with_diffs, :simple, source_project: @project)
+ end
+
+ step 'I am signed in as a developer of the project' do
+ login_as(@user)
+ end
+
+ step 'I should see merge request merged' do
+ expect(page).to have_content('The changes were merged into')
+ end
+end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index d76891d5bde..37bf52b4a95 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -37,7 +37,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'I should see new project avatar' do
expect(@project.avatar).to be_instance_of AvatarUploader
url = @project.avatar.url
- expect(url).to eq "/uploads/project/avatar/#{ @project.id }/banana_sample.gif"
+ expect(url).to eq "/uploads/project/avatar/#{@project.id}/banana_sample.gif"
end
step 'I should see the "Remove avatar" button' do
@@ -124,11 +124,11 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see back to dashboard button' do
- expect(page).to have_content 'Back to dashboard'
+ expect(page).to have_content 'Go to dashboard'
end
step 'I should see back to group button' do
- expect(page).to have_content 'Back to group'
+ expect(page).to have_content 'Go to group'
end
step 'I click notifications drop down button' do
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 1c700df0c63..536199ddb4f 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -11,7 +11,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(page).to have_content 'Project services'
expect(page).to have_content 'Campfire'
expect(page).to have_content 'HipChat'
- expect(page).to have_content 'GitLab CI'
expect(page).to have_content 'Assembla'
expect(page).to have_content 'Pushover'
expect(page).to have_content 'Atlassian Bamboo'
@@ -20,15 +19,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(page).to have_content 'Irker (IRC gateway)'
end
- step 'I click gitlab-ci service link' do
- click_link 'GitLab CI'
- end
-
- step 'I fill gitlab-ci settings' do
- check 'Active'
- click_button 'Save'
- end
-
step 'I should see service settings saved' do
expect(find_field('Active').value).to eq '1'
end
@@ -183,6 +173,24 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Sound').find('option[selected]').value).to eq 'bike'
end
+ step 'I click jira service link' do
+ click_link 'JIRA'
+ end
+
+ step 'I fill jira settings' do
+ fill_in 'Project url', with: 'http://jira.example'
+ fill_in 'Username', with: 'gitlab'
+ fill_in 'Password', with: 'gitlab'
+ fill_in 'Api url', with: 'http://jira.example/rest/api/2'
+ click_button 'Save'
+ end
+
+ step 'I should see jira service settings saved' do
+ expect(find_field('Project url').value).to eq 'http://jira.example'
+ expect(find_field('Username').value).to eq 'gitlab'
+ expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2'
+ end
+
step 'I click Atlassian Bamboo CI service link' do
click_link 'Atlassian Bamboo CI'
end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index db8ad08bb9e..504654f90dd 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -22,7 +22,7 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "New Snippet"' do
- click_link "Add new snippet"
+ click_link "New Snippet"
end
step 'I click link "Snippet one"' do
@@ -42,13 +42,13 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
end
step 'I click link "Edit"' do
- page.within ".file-title" do
+ page.within ".detail-page-header" do
click_link "Edit"
end
end
- step 'I click link "Remove Snippet"' do
- click_link "remove"
+ step 'I click link "Delete"' do
+ click_link "Delete"
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 1b27500497a..d08935aa101 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -5,6 +5,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedPaths
include RepoHelpers
+ step "I don't have write access" do
+ @project = create(:project, name: "Other Project", path: "other-project")
+ @project.team << [@user, :reporter]
+ visit namespace_project_tree_path(@project.namespace, @project, root_ref)
+ end
+
step 'I should see files from repository' do
expect(page).to have_content "VERSION"
expect(page).to have_content ".gitignore"
@@ -37,6 +43,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content new_gitignore_content
end
+ step 'I should see its content with new lines preserved at end of file' do
+ expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
+ end
+
step 'I click link "Raw"' do
click_link 'Raw'
end
@@ -53,10 +63,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).not_to have_link 'edit'
end
- step 'The edit button is disabled' do
- expect(page).to have_css '.disabled', text: 'Edit'
- end
-
step 'I can edit code' do
set_new_content
expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
@@ -66,24 +72,32 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
set_new_content
end
+ step 'I edit code with new lines at end of file' do
+ execute_script('blob.editor.setValue("Sample\n\n\n")')
+ end
+
step 'I fill the new file name' do
fill_in :file_name, with: new_file_name
end
step 'I fill the new branch name' do
- fill_in :new_branch, with: 'new_branch_name', visible: true
+ fill_in :target_branch, with: 'new_branch_name', visible: true
end
step 'I fill the new file name with an illegal name' do
fill_in :file_name, with: 'Spaces Not Allowed'
end
+ step 'I fill the new file name with a new directory' do
+ fill_in :file_name, with: new_file_name_with_directory
+ end
+
step 'I fill the commit message' do
- fill_in :commit_message, with: 'Not yet a commit message.', visible: true
+ fill_in :commit_message, with: 'New commit message', visible: true
end
step 'I click link "Diff"' do
- click_link 'Preview changes'
+ click_link 'Preview Changes'
end
step 'I click on "Commit Changes"' do
@@ -94,16 +108,16 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Create directory'
end
- step 'I click on "Remove"' do
- click_button 'Remove'
+ step 'I click on "Delete"' do
+ click_on 'Delete'
end
- step 'I click on "Remove file"' do
- click_button 'Remove file'
+ step 'I click on "Delete file"' do
+ click_button 'Delete file'
end
step 'I click on "Replace"' do
- click_button "Replace"
+ click_on "Replace"
end
step 'I click on "Replace file"' do
@@ -116,7 +130,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I click on "New file" link in repo' do
find('.add-to-tree').click
- click_link 'Create file'
+ click_link 'New file'
end
step 'I click on "Upload file" link in repo' do
@@ -138,7 +152,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I can see new file page' do
- expect(page).to have_content "new file"
+ expect(page).to have_content "New File"
expect(page).to have_content "Commit message"
end
@@ -147,7 +161,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I can see the new commit message' do
- expect(page).to have_content "New upload commit message"
+ expect(page).to have_content "New commit message"
end
step 'I upload a new text file' do
@@ -156,7 +170,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I fill the upload file commit message' do
page.within('#modal-upload-blob') do
- fill_in :commit_message, with: 'New upload commit message'
+ fill_in :commit_message, with: 'New commit message'
end
end
@@ -188,7 +202,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I see Browse dir link' do
- expect(page).to have_link 'Browse Dir »'
+ expect(page).to have_link 'Browse Directory »'
expect(page).not_to have_link 'Browse Code »'
end
@@ -200,13 +214,13 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I see Browse file link' do
expect(page).to have_link 'Browse File »'
- expect(page).not_to have_link 'Browse Code »'
+ expect(page).not_to have_link 'Browse Files »'
end
step 'I see Browse code link' do
- expect(page).to have_link 'Browse Code »'
+ expect(page).to have_link 'Browse Files »'
expect(page).not_to have_link 'Browse File »'
- expect(page).not_to have_link 'Browse Dir »'
+ expect(page).not_to have_link 'Browse Directory »'
end
step 'I click on Permalink' do
@@ -221,10 +235,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'master/.gitignore'))
end
- step 'I am redirected to the ".gitignore" on new branch' do
- expect(current_path).to eq(namespace_project_blob_path(@project.namespace, @project, 'new_branch_name/.gitignore'))
- end
-
step 'I am redirected to the permalink URL' do
expect(current_path).to(
eq(namespace_project_blob_path(@project.namespace, @project,
@@ -234,29 +244,27 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I am redirected to the new file' do
- expect(current_path).to eq(namespace_project_blob_path(
- @project.namespace, @project, 'master/' + new_file_name))
+ expect(current_path).to eq(
+ namespace_project_blob_path(@project.namespace, @project, 'master/' + new_file_name))
end
- step 'I am redirected to the new file on new branch' do
- expect(current_path).to eq(namespace_project_blob_path(
- @project.namespace, @project, 'new_branch_name/' + new_file_name))
+ step 'I am redirected to the new file with directory' do
+ expect(current_path).to eq(
+ namespace_project_blob_path(@project.namespace, @project, 'master/' + new_file_name_with_directory))
end
- step 'I am redirected to the uploaded file on new branch' do
- expect(current_path).to eq(namespace_project_blob_path(
- @project.namespace, @project,
- 'new_branch_name/' + File.basename(test_text_file)))
+ step 'I am redirected to the new merge request page' do
+ expect(current_path).to eq(new_namespace_project_merge_request_path(@project.namespace, @project))
end
- step 'I am redirected to the new directory' do
- expect(current_path).to eq(namespace_project_tree_path(
- @project.namespace, @project, 'new_branch_name/' + new_dir_name))
+ step "I am redirected to the fork's new merge request page" do
+ fork = @user.fork_of(@project)
+ expect(current_path).to eq(new_namespace_project_merge_request_path(fork.namespace, fork))
end
step 'I am redirected to the root directory' do
- expect(current_path).to eq(namespace_project_tree_path(
- @project.namespace, @project, 'master/'))
+ expect(current_path).to eq(
+ namespace_project_tree_path(@project.namespace, @project, 'master'))
end
step "I don't see the permalink link" do
@@ -312,6 +320,37 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).not_to have_content('Loading commit data...')
end
+ step 'I click on "files/lfs/lfs_object.iso" file in repo' do
+ visit namespace_project_tree_path(@project.namespace, @project, "lfs")
+ click_link 'files'
+ click_link "lfs"
+ click_link "lfs_object.iso"
+ end
+
+ step 'I should see download link and object size' do
+ expect(page).to have_content 'Download (1.5 MB)'
+ end
+
+ step 'I should not see lfs pointer details' do
+ expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1'
+ expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897'
+ expect(page).not_to have_content 'size 1575078'
+ 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'
+ end
+
+ step 'I should see a notice about a new fork having been created' do
+ expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request."
+ end
+
private
def set_new_content
@@ -335,6 +374,12 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
'not_a_file.md'
end
+ # Constant value that is a valid filename with directory and
+ # not a filename present at root of the seed repository.
+ def new_file_name_with_directory
+ 'foo/bar/baz.txt'
+ end
+
# Constant value that is a valid directory and
# not a directory present at root of the seed repository.
def new_dir_name
diff --git a/features/steps/project/source/markdown_render.rb b/features/steps/project/source/markdown_render.rb
index c78e86fa1a7..3a4f7a6e01c 100644
--- a/features/steps/project/source/markdown_render.rb
+++ b/features/steps/project/source/markdown_render.rb
@@ -238,7 +238,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see new wiki page named test' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "test")
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page test"
end
When 'I go back to wiki page home' do
@@ -252,7 +252,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Gitlab API document' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "api")
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page api"
end
step 'I click on Rake tasks link' do
@@ -261,7 +261,7 @@ class Spinach::Features::ProjectSourceMarkdownRender < Spinach::FeatureSteps
step 'I see Rake tasks directory' do
expect(current_path).to eq namespace_project_wiki_path(@project.namespace, @project, "raketasks")
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page raketasks"
end
step 'I go directory which contains README file' do
diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb
index bd2e0619cdd..9f7c748a3b7 100644
--- a/features/steps/project/star.rb
+++ b/features/steps/project/star.rb
@@ -32,6 +32,6 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps
protected
def has_n_stars(n)
- expect(page).to have_css(".star-btn .count", text: n, visible: true)
+ expect(page).to have_css(".star-count", text: n, visible: true)
end
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 97d63016458..caad52def79 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -15,10 +15,6 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
expect(page).to have_content(user.username)
end
- step 'I click link "Add members"' do
- find(:css, 'button.btn-new').click
- end
-
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 02207dbffa6..91d227fadbf 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -5,7 +5,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
include SharedPaths
step 'I click on the Cancel button' do
- page.within(:css, ".form-actions") do
+ page.within(:css, ".wiki-form .form-actions") do
click_on "Cancel"
end
end
@@ -24,7 +24,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(page).to have_content "link test"
click_link "link test"
- expect(page).to have_content "Editing"
+ expect(page).to have_content "Edit Page"
end
step 'I have an existing Wiki page' do
@@ -68,7 +68,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I click on the "Delete this page" button' do
- click_on "Delete this page"
+ click_on "Delete"
end
step 'The page should be deleted' do
@@ -120,13 +120,13 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I should see the new wiki page form' do
expect(current_path).to match('wikis/image.jpg')
expect(page).to have_content('New Wiki Page')
- expect(page).to have_content('Editing - image.jpg')
+ expect(page).to have_content('Edit Page image.jpg')
end
step 'I create a New page with paths' do
click_on 'New Page'
fill_in 'Page slug', with: 'one/two/three'
- click_on 'Build'
+ click_on 'Create Page'
fill_in "wiki_content", with: 'wiki content'
click_on "Create page"
expect(current_path).to include 'one/two/three'
@@ -135,7 +135,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
step 'I create a New page with an invalid name' do
click_on 'New Page'
fill_in 'Page slug', with: 'invalid name'
- click_on 'Build'
+ click_on 'Create Page'
end
step 'I should see an error message' do
@@ -156,7 +156,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'I should see the Editing page' do
- expect(page).to have_content('Editing')
+ expect(page).to have_content('Edit Page')
end
step 'I view the page history of a Wiki page that has a path' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 72621911a37..c6a0ae2ba38 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -87,6 +87,17 @@ module SharedDiffNote
end
end
+ step 'I write a diff comment like ":smile:"' do
+ page.within(diff_file_selector) do
+ click_diff_line(sample_commit.line_code)
+
+ page.within("form[rel$='#{sample_commit.line_code}']") do
+ fill_in 'note[note]', with: ':smile:'
+ click_button('Add Comment')
+ end
+ end
+ end
+
step 'I submit the diff comment' do
page.within(diff_file_selector) do
click_button("Add Comment")
@@ -155,7 +166,7 @@ module SharedDiffNote
end
step 'I should see add a diff comment button' do
- expect(page).to have_css('.js-add-diff-note-button', visible: true)
+ expect(page).to have_css('.js-add-diff-note-button')
end
step 'I should see an empty diff comment form' do
@@ -197,6 +208,12 @@ module SharedDiffNote
end
end
+ step 'I should see a diff comment with an emoji image' do
+ page.within("#{diff_file_selector} .note") do
+ expect(page).to have_xpath("//img[@alt=':smile:']")
+ end
+ end
+
step 'I click side-by-side diff button' do
find('#parallel-diff-btn').trigger('click')
end
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index 83a04576973..fe6736dacd4 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -1,6 +1,10 @@
module SharedGroup
include Spinach::DSL
+ step 'current user is developer of group "Owned"' do
+ is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER)
+ end
+
step '"John Doe" is owner of group "Owned"' do
is_member_of("John Doe", "Owned", Gitlab::Access::OWNER)
end
@@ -41,4 +45,8 @@ module SharedGroup
project.team << [user, :master]
@project_count += 1
end
+
+ def owned_group
+ @owned_group ||= Group.find_by(name: "Owned")
+ end
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index eb978620da6..b33bd332655 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -31,6 +31,10 @@ module SharedPaths
visit merge_requests_group_path(Group.find_by(name: "Owned"))
end
+ step 'I visit group "Owned" milestones page' do
+ visit group_milestones_path(Group.find_by(name: "Owned"))
+ end
+
step 'I visit group "Owned" members page' do
visit group_group_members_path(Group.find_by(name: "Owned"))
end
@@ -208,8 +212,8 @@ module SharedPaths
end
step 'I visit a binary file in the repo' do
- visit namespace_project_blob_path(@project.namespace, @project, File.join(
- root_ref, 'files/images/logo-black.png'))
+ visit namespace_project_blob_path(@project.namespace, @project,
+ File.join(root_ref, 'files/images/logo-black.png'))
end
step "I visit my project's commits page" do
@@ -312,8 +316,8 @@ module SharedPaths
end
step 'I am on the ".gitignore" edit file page' do
- expect(current_path).to eq(namespace_project_edit_blob_path(
- @project.namespace, @project, File.join(root_ref, '.gitignore')))
+ expect(current_path).to eq(
+ namespace_project_edit_blob_path(@project.namespace, @project, File.join(root_ref, '.gitignore')))
end
step 'I visit project source page for "6d39438"' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 7021fac5fe4..da643bf3ba9 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -204,7 +204,7 @@ module SharedProject
step 'project "Shop" has CI build' do
project = Project.find_by(name: "Shop")
- create :ci_commit, gl_project: project, sha: project.commit.sha
+ create :ci_commit, project: project, sha: project.commit.sha
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 c67e5e4a06a..4fc2ece79ff 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -16,10 +16,6 @@ module SharedProjectTab
ensure_active_main_tab('Commits')
end
- step 'the active main tab should be Network' do
- ensure_active_main_tab('Network')
- end
-
step 'the active main tab should be Graphs' do
ensure_active_main_tab('Graphs')
end
@@ -46,11 +42,15 @@ module SharedProjectTab
step 'the active main tab should be Settings' do
page.within '.nav-sidebar' do
- expect(page).to have_content('Back to project')
+ expect(page).to have_content('Go to project')
end
end
step 'the active main tab should be Activity' do
ensure_active_main_tab('Activity')
end
+
+ step 'the active sub tab should be Network' do
+ ensure_active_sub_tab('Network')
+ end
end
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
index fc1e8d6e889..f0721094ee3 100644
--- a/features/steps/shared/user.rb
+++ b/features/steps/shared/user.rb
@@ -9,9 +9,21 @@ module SharedUser
user_exists("Mary Jane", { username: "mary_jane" })
end
+ step 'gitlab user "Mike"' do
+ create(:user, name: "Mike")
+ end
+
protected
def user_exists(name, options = {})
User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options))
end
+
+ step 'I have an ssh key' do
+ create(:personal_key, user: @user)
+ end
+
+ step 'I have no ssh keys' do
+ @user.keys.delete_all
+ end
end
diff --git a/features/steps/snippets/snippets.rb b/features/steps/snippets/snippets.rb
index 6ff48e0c6b8..023032e679f 100644
--- a/features/steps/snippets/snippets.rb
+++ b/features/steps/snippets/snippets.rb
@@ -13,13 +13,13 @@ class Spinach::Features::Snippets < Spinach::FeatureSteps
end
step 'I click link "Edit"' do
- page.within ".file-title" do
+ page.within ".detail-page-header" do
click_link "Edit"
end
end
- step 'I click link "Destroy"' do
- click_link "remove"
+ step 'I click link "Delete"' do
+ click_link "Delete"
end
step 'I submit new snippet "Personal snippet three"' do
diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb
index dea3256229f..997c605bce2 100644
--- a/features/steps/snippets/user.rb
+++ b/features/steps/snippets/user.rb
@@ -32,19 +32,19 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
end
step 'I click "Internal" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Internal"
end
end
step 'I click "Private" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Private"
end
end
step 'I click "Public" filter' do
- page.within('.nav-tabs') do
+ page.within('.snippet-scope-menu') do
click_link "Public"
end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
index 31dbf0feb2f..4156c7ec484 100644
--- a/features/support/capybara.rb
+++ b/features/support/capybara.rb
@@ -2,7 +2,7 @@ require 'spinach/capybara'
require 'capybara/poltergeist'
# Give CI some extra time
-timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 15
Capybara.javascript_driver = :poltergeist
Capybara.register_driver :poltergeist do |app|
diff --git a/fixtures/emojis/aliases.json b/fixtures/emojis/aliases.json
new file mode 100644
index 00000000000..547ce7978b3
--- /dev/null
+++ b/fixtures/emojis/aliases.json
@@ -0,0 +1,367 @@
+{
+ "northeast_pointing_airplane":"airplane_northeast",
+ "small_airplane":"airplane_small",
+ "up_pointing_small_airplane":"airplane_small_up",
+ "up_pointing_airplane":"airplane_up",
+ "left_anger_bubble":"anger_left",
+ "right_anger_bubble":"anger_right",
+ "ballot_box_with_ballot":"ballot_box",
+ "ballot_box_with_bold_check":"ballot_box_check",
+ "ballot_box_with_script_x":"ballot_box_x",
+ "ballot_script_x":"ballot_x",
+ "beach_with_umbrella":"beach",
+ "bellhop_bell":"bellhop",
+ "bouquet_of_flowers":"bouquet2",
+ "bullhorn_with_sound_waves":"bullhorn_waves",
+ "pocket calculator":"calculator",
+ "spiral_calendar_pad":"calendar_spiral",
+ "card_file_box":"card_box",
+ "tape_cartridge":"cartridge",
+ "city_sunrise":"city_sunset",
+ "mantlepiece_clock":"clock",
+ "clockwise_right_and_left_semicircle_arrows":"clockwise_arrows",
+ "cloud_with_lightning":"cloud_lightning",
+ "cloud_with_rain":"cloud_rain",
+ "cloud_with_snow":"cloud_snow",
+ "cloud_with_tornado":"cloud_tornado",
+ "old_personal_computer":"computer_old",
+ "building_construction":"contruction_site",
+ "couch_and_lamp":"couch",
+ "couple_with_heart_mm":"couple_mm",
+ "couple_with_heart_ww":"couple_ww",
+ "lower_left_crayon":"crayon",
+ "heavy_latin_cross":"cross_heavy",
+ "white_latin_cross":"cross_white",
+ "black_skull_and_crossbones":"crossbones",
+ "passenger_ship":"cruise_ship",
+ "dagger_knife":"dagger",
+ "desktop_computer":"desktop",
+ "card_index_dividers":"dividers",
+ "document_with_text":"document_text",
+ "dove_of_peace":"dove",
+ "email":"e-mail",
+ "back_of_envelope":"envelope_back",
+ "flying_envelope":"envelope_flying",
+ "stamped_envelope":"envelope_stamped",
+ "pen_over_stamped_envelope":"envelope_stamped_pen",
+ "white_down_pointing_left_hand_index":"finger_pointing_down",
+ "sideways_white_down_pointing_index":"finger_pointing_down2",
+ "sideways_white_left_pointing_index":"finger_pointing_left",
+ "sideways_white_right_pointing_index":"finger_pointing_right",
+ "sideways_white_up_pointing_index":"finger_pointing_up",
+ "flame":"fire",
+ "oncoming_fire_engine":"fire_engine_oncoming",
+ "ac":"flag_ac",
+ "ad":"flag_ad",
+ "ae":"flag_ae",
+ "af":"flag_af",
+ "ag":"flag_ag",
+ "ai":"flag_ai",
+ "al":"flag_al",
+ "am":"flag_am",
+ "ao":"flag_ao",
+ "ar":"flag_ar",
+ "at":"flag_at",
+ "au":"flag_au",
+ "aw":"flag_aw",
+ "az":"flag_az",
+ "ba":"flag_ba",
+ "bb":"flag_bb",
+ "bd":"flag_bd",
+ "be":"flag_be",
+ "bf":"flag_bf",
+ "bg":"flag_bg",
+ "bh":"flag_bh",
+ "bi":"flag_bi",
+ "bj":"flag_bj",
+ "waving_black_flag":"flag_black",
+ "bm":"flag_bm",
+ "bn":"flag_bn",
+ "bo":"flag_bo",
+ "br":"flag_br",
+ "bs":"flag_bs",
+ "bt":"flag_bt",
+ "bw":"flag_bw",
+ "by":"flag_by",
+ "bz":"flag_bz",
+ "ca":"flag_ca",
+ "congo":"flag_cd",
+ "cf":"flag_cf",
+ "cg":"flag_cg",
+ "ch":"flag_ch",
+ "ci":"flag_ci",
+ "chile":"flag_cl",
+ "cm":"flag_cm",
+ "cn":"flag_cn",
+ "co":"flag_co",
+ "cr":"flag_cr",
+ "cu":"flag_cu",
+ "cv":"flag_cv",
+ "cy":"flag_cy",
+ "cz":"flag_cz",
+ "de":"flag_de",
+ "dj":"flag_dj",
+ "dk":"flag_dk",
+ "dm":"flag_dm",
+ "do":"flag_do",
+ "dz":"flag_dz",
+ "ec":"flag_ec",
+ "ee":"flag_ee",
+ "eg":"flag_eg",
+ "eh":"flag_eh",
+ "er":"flag_er",
+ "es":"flag_es",
+ "et":"flag_et",
+ "fi":"flag_fi",
+ "fj":"flag_fj",
+ "fk":"flag_fk",
+ "fm":"flag_fm",
+ "fo":"flag_fo",
+ "fr":"flag_fr",
+ "ga":"flag_ga",
+ "gb":"flag_gb",
+ "gd":"flag_gd",
+ "ge":"flag_ge",
+ "gh":"flag_gh",
+ "gi":"flag_gi",
+ "gl":"flag_gl",
+ "gm":"flag_gm",
+ "gn":"flag_gn",
+ "gq":"flag_gq",
+ "gr":"flag_gr",
+ "gt":"flag_gt",
+ "gu":"flag_gu",
+ "gw":"flag_gw",
+ "gy":"flag_gy",
+ "hk":"flag_hk",
+ "hn":"flag_hn",
+ "hr":"flag_hr",
+ "ht":"flag_ht",
+ "hu":"flag_hu",
+ "indonesia":"flag_id",
+ "ie":"flag_ie",
+ "il":"flag_il",
+ "in":"flag_in",
+ "iq":"flag_iq",
+ "ir":"flag_ir",
+ "is":"flag_is",
+ "it":"flag_it",
+ "je":"flag_je",
+ "jm":"flag_jm",
+ "jo":"flag_jo",
+ "jp":"flag_jp",
+ "ke":"flag_ke",
+ "kg":"flag_kg",
+ "kh":"flag_kh",
+ "ki":"flag_ki",
+ "km":"flag_km",
+ "kn":"flag_kn",
+ "kp":"flag_kp",
+ "kr":"flag_kr",
+ "kw":"flag_kw",
+ "ky":"flag_ky",
+ "kz":"flag_kz",
+ "la":"flag_la",
+ "lb":"flag_lb",
+ "lc":"flag_lc",
+ "li":"flag_li",
+ "lk":"flag_lk",
+ "lr":"flag_lr",
+ "ls":"flag_ls",
+ "lt":"flag_lt",
+ "lu":"flag_lu",
+ "lv":"flag_lv",
+ "ly":"flag_ly",
+ "ma":"flag_ma",
+ "mc":"flag_mc",
+ "md":"flag_md",
+ "me":"flag_me",
+ "mg":"flag_mg",
+ "mh":"flag_mh",
+ "mk":"flag_mk",
+ "ml":"flag_ml",
+ "mm":"flag_mm",
+ "mn":"flag_mn",
+ "mo":"flag_mo",
+ "mr":"flag_mr",
+ "ms":"flag_ms",
+ "mt":"flag_mt",
+ "mu":"flag_mu",
+ "mv":"flag_mv",
+ "mw":"flag_mw",
+ "mx":"flag_mx",
+ "my":"flag_my",
+ "mz":"flag_mz",
+ "na":"flag_na",
+ "nc":"flag_nc",
+ "ne":"flag_ne",
+ "nigeria":"flag_ng",
+ "ni":"flag_ni",
+ "nl":"flag_nl",
+ "no":"flag_no",
+ "np":"flag_np",
+ "nr":"flag_nr",
+ "nu":"flag_nu",
+ "nz":"flag_nz",
+ "om":"flag_om",
+ "pa":"flag_pa",
+ "pe":"flag_pe",
+ "pf":"flag_pf",
+ "pg":"flag_pg",
+ "ph":"flag_ph",
+ "pk":"flag_pk",
+ "pl":"flag_pl",
+ "pr":"flag_pr",
+ "ps":"flag_ps",
+ "pt":"flag_pt",
+ "pw":"flag_pw",
+ "py":"flag_py",
+ "qa":"flag_qa",
+ "ro":"flag_ro",
+ "rs":"flag_rs",
+ "ru":"flag_ru",
+ "rw":"flag_rw",
+ "saudiarabia":"flag_sa",
+ "saudi":"flag_sa",
+ "sb":"flag_sb",
+ "sc":"flag_sc",
+ "sd":"flag_sd",
+ "se":"flag_se",
+ "sg":"flag_sg",
+ "sh":"flag_sh",
+ "si":"flag_si",
+ "sk":"flag_sk",
+ "sl":"flag_sl",
+ "sm":"flag_sm",
+ "sn":"flag_sn",
+ "so":"flag_so",
+ "sr":"flag_sr",
+ "st":"flag_st",
+ "sv":"flag_sv",
+ "sy":"flag_sy",
+ "sz":"flag_sz",
+ "td":"flag_td",
+ "tg":"flag_tg",
+ "th":"flag_th",
+ "tj":"flag_tj",
+ "tl":"flag_tl",
+ "turkmenistan":"flag_tm",
+ "tn":"flag_tn",
+ "to":"flag_to",
+ "tr":"flag_tr",
+ "tt":"flag_tt",
+ "tuvalu":"flag_tv",
+ "tw":"flag_tw",
+ "tz":"flag_tz",
+ "ua":"flag_ua",
+ "ug":"flag_ug",
+ "us":"flag_us",
+ "uy":"flag_uy",
+ "uz":"flag_uz",
+ "va":"flag_va",
+ "vc":"flag_vc",
+ "ve":"flag_ve",
+ "vi":"flag_vi",
+ "vn":"flag_vn",
+ "vu":"flag_vu",
+ "wf":"flag_wf",
+ "waving_white_flag":"flag_white",
+ "ws":"flag_ws",
+ "xk":"flag_xk",
+ "ye":"flag_ye",
+ "za":"flag_za",
+ "zm":"flag_zm",
+ "zw":"flag_zw",
+ "clamshell_mobile_phone":"flip_phone",
+ "black_hard_shell_floppy_disk":"floppy_black",
+ "white_hard_shell_floppy_disk":"floppy_white",
+ "open_folder":"folder_open",
+ "fork_and_knife_with_plate":"fork_knife_plate",
+ "frame_with_picture":"frame_photo",
+ "frame_with_tiles":"frame_tiles",
+ "frame_with_an_x":"frame_x",
+ "anguished":"frowning",
+ "raised_hand_with_fingers_splayed":"hand_splayed",
+ "reversed_raised_hand_with_fingers_splayed":"hand_splayed_reverse",
+ "reversed_victory_hand":"hand_victory",
+ "heart_with_tip_on_the_left":"heart_tip",
+ "house_buildings":"homes",
+ "derelict_house_building":"house_abandoned",
+ "circled_information_source":"info",
+ "desert_island":"island",
+ "up_pointing_military_airplane":"jet_up",
+ "old_key":"key2",
+ "wired_keyboard":"keyboard",
+ "keyboard_and_mouse":"keyboard_mouse",
+ "musical_keyboard_with_jacks":"keyboard_with_jacks",
+ "couplekiss_mm":"kiss_mm",
+ "couplekiss_ww":"kiss_ww",
+ "satisfied":"laughing",
+ "left_hand_telephone_receiver":"left_receiver",
+ "man_in_business_suit_levitating":"levitate",
+ "weight_lifter":"lifter",
+ "light_mark":"light_check_mark",
+ "world_map":"map",
+ "sports_medal":"medal",
+ "studio_microphone":"microphone2",
+ "reversed_hand_with_middle_finger_extended":"middle_finger",
+ "lightning_mood_bubble":"mood_bubble_lightning",
+ "lightning_mood":"mood_lightning",
+ "racing_motorcycle":"motorcycle",
+ "snow_capped_mountain":"mountain_snow",
+ "one_button_mouse":"mouse_one",
+ "three_networked_computers":"network",
+ "rolled_up_newspaper":"newspaper2",
+ "note_page":"note",
+ "empty_note_page":"note_empty",
+ "note_pad":"notepad",
+ "empty_note_pad":"notepad_empty",
+ "spiral_note_pad":"notepad_spiral",
+ "oil_drum":"oil",
+ "grandma":"older_woman",
+ "optical_disc_icon":"optical_disk",
+ "lower_left_paintbrush":"paintbrush",
+ "linked_paperclips":"paperclips",
+ "national_park":"park",
+ "lower_left_ballpoint_pen":"pen_ballpoint",
+ "lower_left_fountain_pen":"pen_fountain",
+ "memo":"pencil",
+ "lower_left_pencil":"pencil3",
+ "black_pennant":"pennant_black",
+ "white_pennant":"pennant_white",
+ "no_piracy":"piracy",
+ "shit":"poop",
+ "hankey":"poop",
+ "poo":"poop",
+ "prohibited_sign":"prohibited",
+ "film_projector":"projector",
+ "racing_car":"race_car",
+ "railroad_track":"railway_track",
+ "right_speaker_with_one_sound_wave":"right_speaker_one",
+ "right_speaker_with_three_sound_waves":"right_speaker_three",
+ "skeleton":"skull",
+ "slightly_frowning_face":"slight_frown",
+ "slightly_smiling_face":"slight_smile",
+ "speaking_head_in_silhouette":"speaking_head",
+ "left_speech_bubble":"speech_left",
+ "right_speech_bubble":"speech_right",
+ "three_speech_bubbles":"speech_three",
+ "two_speech_bubbles":"speech_two",
+ "sleuth_or_spy":"spy",
+ "portable_stereo":"stereo",
+ "black_touchtone_telephone":"telephone_black",
+ "white_touchtone_telephone":"telephone_white",
+ "left_thought_bubble":"thought_left",
+ "right_thought_bubble":"thought_right",
+ "reversed_thumbs_down_sign":"thumbs_down_reverse",
+ "reversed_thumbs_up_sign":"thumbs_up_reverse",
+ "-1":"thumbsdown",
+ "+1":"thumbsup",
+ "admission_tickets":"tickets",
+ "hammer_and_wrench":"tools",
+ "diesel_locomotive":"train_diesel",
+ "triangle_with_rounded_corners":"triangle_round",
+ "turned_ok_hand_sign":"turned_ok_hand",
+ "raised_hand_with_part_between_middle_and_ring_fingers":"vulcan",
+ "left_writing_hand":"writing_hand"
+} \ No newline at end of file
diff --git a/fixtures/emojis/index.json b/fixtures/emojis/index.json
new file mode 100644
index 00000000000..60ef2399e14
--- /dev/null
+++ b/fixtures/emojis/index.json
@@ -0,0 +1,13376 @@
+{
+ "100": {
+ "unicode": "1F4AF",
+ "unicode_alternates": [],
+ "name": "hundred points symbol",
+ "shortname": ":100:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["numbers", "perfect", "score", "100", "percent", "a", "plus", "perfect", "school", "quiz", "score", "test", "exam"],
+ "moji": "💯"
+ },
+ "1234": {
+ "unicode": "1F522",
+ "unicode_alternates": [],
+ "name": "input symbol for numbers",
+ "shortname": ":1234:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "numbers"],
+ "moji": "🔢"
+ },
+ "8ball": {
+ "unicode": "1F3B1",
+ "unicode_alternates": [],
+ "name": "billiards",
+ "shortname": ":8ball:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["pool", "billiards", "eight ball", "pool", "pocket ball", "cue"],
+ "moji": "🎱"
+ },
+ "a": {
+ "unicode": "1F170",
+ "unicode_alternates": [],
+ "name": "negative squared latin capital letter a",
+ "shortname": ":a:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "letter", "red-square"],
+ "moji": "🅰"
+ },
+ "ab": {
+ "unicode": "1F18E",
+ "unicode_alternates": [],
+ "name": "negative squared ab",
+ "shortname": ":ab:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "red-square"],
+ "moji": "🆎"
+ },
+ "abc": {
+ "unicode": "1F524",
+ "unicode_alternates": [],
+ "name": "input symbol for latin letters",
+ "shortname": ":abc:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "blue-square"],
+ "moji": "🔤"
+ },
+ "abcd": {
+ "unicode": "1F521",
+ "unicode_alternates": [],
+ "name": "input symbol for latin small letters",
+ "shortname": ":abcd:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "blue-square"],
+ "moji": "🔡"
+ },
+ "accept": {
+ "unicode": "1F251",
+ "unicode_alternates": [],
+ "name": "circled ideograph accept",
+ "shortname": ":accept:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["agree", "chinese", "good", "kanji", "ok", "yes"],
+ "moji": "🉑"
+ },
+ "aerial_tramway": {
+ "unicode": "1F6A1",
+ "unicode_alternates": [],
+ "name": "aerial tramway",
+ "shortname": ":aerial_tramway:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "aerial", "tram", "tramway", "cable", "transport"],
+ "moji": "🚡"
+ },
+ "airplane": {
+ "unicode": "2708",
+ "unicode_alternates": ["2708-FE0F"],
+ "name": "airplane",
+ "shortname": ":airplane:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flight", "transportation", "vehicle", "airplane", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"],
+ "moji": "✈"
+ },
+ "airplane_arriving": {
+ "unicode": "1F6EC",
+ "unicode_alternates": [],
+ "name": "airplane arriving",
+ "shortname": ":airplane_arriving:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"]
+ },
+ "airplane_departure": {
+ "unicode": "1F6EB",
+ "unicode_alternates": [],
+ "name": "airplane departure",
+ "shortname": ":airplane_departure:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus", "leaving"]
+ },
+ "airplane_northeast": {
+ "unicode": "1F6EA",
+ "unicode_alternates": [],
+ "name": "northeast-pointing airplane",
+ "shortname": ":airplane_northeast:",
+ "category": "travel_places",
+ "aliases": [":northeast_pointing_airplane:"],
+ "aliases_ascii": [],
+ "keywords": ["plane", "travel"]
+ },
+ "airplane_small": {
+ "unicode": "1F6E9",
+ "unicode_alternates": [],
+ "name": "small airplane",
+ "shortname": ":airplane_small:",
+ "category": "travel_places",
+ "aliases": [":small_airplane:"],
+ "aliases_ascii": [],
+ "keywords": ["flight", "transportation", "vehicle", "plane", "airport", "travel", "airlines", "fly", "jet", "jumbo", "boeing", "airbus"]
+ },
+ "airplane_small_up": {
+ "unicode": "1F6E8",
+ "unicode_alternates": [],
+ "name": "up-pointing small airplane",
+ "shortname": ":airplane_small_up:",
+ "category": "travel_places",
+ "aliases": [":up_pointing_small_airplane:"],
+ "aliases_ascii": [],
+ "keywords": ["plane", "travel"]
+ },
+ "airplane_up": {
+ "unicode": "1F6E7",
+ "unicode_alternates": [],
+ "name": "up-pointing airplane",
+ "shortname": ":airplane_up:",
+ "category": "travel_places",
+ "aliases": [":up_pointing_airplane:"],
+ "aliases_ascii": [],
+ "keywords": ["plane", "travel"]
+ },
+ "alarm_clock": {
+ "unicode": "23F0",
+ "unicode_alternates": [],
+ "name": "alarm clock",
+ "shortname": ":alarm_clock:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["time", "wake"],
+ "moji": "⏰"
+ },
+ "alien": {
+ "unicode": "1F47D",
+ "unicode_alternates": [],
+ "name": "extraterrestrial alien",
+ "shortname": ":alien:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["UFO", "paul", "alien", "ufo"],
+ "moji": "👽"
+ },
+ "ambulance": {
+ "unicode": "1F691",
+ "unicode_alternates": [],
+ "name": "ambulance",
+ "shortname": ":ambulance:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["911", "health", "ambulance", "emergency", "medical", "help", "assistance"],
+ "moji": "🚑"
+ },
+ "anchor": {
+ "unicode": "2693",
+ "unicode_alternates": ["2693-FE0F"],
+ "name": "anchor",
+ "shortname": ":anchor:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ferry", "ship", "anchor", "ship", "boat", "ocean", "harbor", "marina", "shipyard", "sailor", "tattoo"],
+ "moji": "⚓"
+ },
+ "angel": {
+ "unicode": "1F47C",
+ "unicode_alternates": [],
+ "name": "baby angel",
+ "shortname": ":angel:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["baby", "angel", "halo", "cupid", "wings", "halo", "heaven", "wings", "jesus"],
+ "moji": "👼"
+ },
+ "anger": {
+ "unicode": "1F4A2",
+ "unicode_alternates": [],
+ "name": "anger symbol",
+ "shortname": ":anger:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["anger", "angry", "mad"],
+ "moji": "💢"
+ },
+ "anger_left": {
+ "unicode": "1F5EE",
+ "unicode_alternates": [],
+ "name": "left anger bubble",
+ "shortname": ":anger_left:",
+ "category": "objects_symbols",
+ "aliases": [":left_anger_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"]
+ },
+ "anger_right": {
+ "unicode": "1F5EF",
+ "unicode_alternates": [],
+ "name": "right anger bubble",
+ "shortname": ":anger_right:",
+ "category": "objects_symbols",
+ "aliases": [":right_anger_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["speech", "balloon", "talk", "mood", "conversation", "communication", "comic", "angry"]
+ },
+ "angry": {
+ "unicode": "1F620",
+ "unicode_alternates": [],
+ "name": "angry face",
+ "shortname": ":angry:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [">:(", ">:-(", ":@"],
+ "keywords": ["angry", "livid", "mad", "vexed", "irritated", "annoyed", "face", "frustrated", "mad"],
+ "moji": "😠"
+ },
+ "anguished": {
+ "unicode": "1F627",
+ "unicode_alternates": [],
+ "name": "anguished face",
+ "shortname": ":anguished:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "nervous", "stunned", "pain", "anguish", "ouch", "misery", "distress", "grief"],
+ "moji": "😧"
+ },
+ "ant": {
+ "unicode": "1F41C",
+ "unicode_alternates": [],
+ "name": "ant",
+ "shortname": ":ant:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "insect", "ant", "queen", "insect", "team"],
+ "moji": "🐜"
+ },
+ "apple": {
+ "unicode": "1F34E",
+ "unicode_alternates": [],
+ "name": "red apple",
+ "shortname": ":apple:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fruit", "mac", "apple", "fruit", "electronics", "red", "doctor", "teacher", "school", "core"],
+ "moji": "🍎"
+ },
+ "aquarius": {
+ "unicode": "2652",
+ "unicode_alternates": ["2652-FE0F"],
+ "name": "aquarius",
+ "shortname": ":aquarius:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["aquarius", "water", "bearer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "moji": "♒"
+ },
+ "aries": {
+ "unicode": "2648",
+ "unicode_alternates": ["2648-FE0F"],
+ "name": "aries",
+ "shortname": ":aries:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["aries", "ram", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "moji": "♈"
+ },
+ "arrow_backward": {
+ "unicode": "25C0",
+ "unicode_alternates": ["25C0-FE0F"],
+ "name": "black left-pointing triangle",
+ "shortname": ":arrow_backward:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "◀"
+ },
+ "arrow_double_down": {
+ "unicode": "23EC",
+ "unicode_alternates": [],
+ "name": "black down-pointing double triangle",
+ "shortname": ":arrow_double_down:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "⏬"
+ },
+ "arrow_double_up": {
+ "unicode": "23EB",
+ "unicode_alternates": [],
+ "name": "black up-pointing double triangle",
+ "shortname": ":arrow_double_up:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "⏫"
+ },
+ "arrow_down": {
+ "unicode": "2B07",
+ "unicode_alternates": ["2B07-FE0F"],
+ "name": "downwards black arrow",
+ "shortname": ":arrow_down:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "⬇"
+ },
+ "arrow_down_small": {
+ "unicode": "1F53D",
+ "unicode_alternates": [],
+ "name": "down-pointing small red triangle",
+ "shortname": ":arrow_down_small:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "🔽"
+ },
+ "arrow_forward": {
+ "unicode": "25B6",
+ "unicode_alternates": ["25B6-FE0F"],
+ "name": "black right-pointing triangle",
+ "shortname": ":arrow_forward:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "▶"
+ },
+ "arrow_heading_down": {
+ "unicode": "2935",
+ "unicode_alternates": ["2935-FE0F"],
+ "name": "arrow pointing rightwards then curving downwards",
+ "shortname": ":arrow_heading_down:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "⤵"
+ },
+ "arrow_heading_up": {
+ "unicode": "2934",
+ "unicode_alternates": ["2934-FE0F"],
+ "name": "arrow pointing rightwards then curving upwards",
+ "shortname": ":arrow_heading_up:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "⤴"
+ },
+ "arrow_left": {
+ "unicode": "2B05",
+ "unicode_alternates": ["2B05-FE0F"],
+ "name": "leftwards black arrow",
+ "shortname": ":arrow_left:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square", "previous"],
+ "moji": "⬅"
+ },
+ "arrow_lower_left": {
+ "unicode": "2199",
+ "unicode_alternates": ["2199-FE0F"],
+ "name": "south west arrow",
+ "shortname": ":arrow_lower_left:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "↙"
+ },
+ "arrow_lower_right": {
+ "unicode": "2198",
+ "unicode_alternates": ["2198-FE0F"],
+ "name": "south east arrow",
+ "shortname": ":arrow_lower_right:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "blue-square"],
+ "moji": "↘"
+ },
+ "arrow_right": {
+ "unicode": "27A1",
+ "unicode_alternates": ["27A1-FE0F"],
+ "name": "black rightwards arrow",
+ "shortname": ":arrow_right:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "next"],
+ "moji": "➡"
+ },
+ "arrow_right_hook": {
+ "unicode": "21AA",
+ "unicode_alternates": ["21AA-FE0F"],
+ "name": "rightwards arrow with hook",
+ "shortname": ":arrow_right_hook:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "↪"
+ },
+ "arrow_up": {
+ "unicode": "2B06",
+ "unicode_alternates": ["2B06-FE0F"],
+ "name": "upwards black arrow",
+ "shortname": ":arrow_up:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "⬆"
+ },
+ "arrow_up_down": {
+ "unicode": "2195",
+ "unicode_alternates": ["2195-FE0F"],
+ "name": "up down arrow",
+ "shortname": ":arrow_up_down:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "↕"
+ },
+ "arrow_up_small": {
+ "unicode": "1F53C",
+ "unicode_alternates": [],
+ "name": "up-pointing small red triangle",
+ "shortname": ":arrow_up_small:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "🔼"
+ },
+ "arrow_upper_left": {
+ "unicode": "2196",
+ "unicode_alternates": ["2196-FE0F"],
+ "name": "north west arrow",
+ "shortname": ":arrow_upper_left:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "↖"
+ },
+ "arrow_upper_right": {
+ "unicode": "2197",
+ "unicode_alternates": ["2197-FE0F"],
+ "name": "north east arrow",
+ "shortname": ":arrow_upper_right:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "↗"
+ },
+ "arrows_clockwise": {
+ "unicode": "1F503",
+ "unicode_alternates": [],
+ "name": "clockwise downwards and upwards open circle arrows",
+ "shortname": ":arrows_clockwise:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sync"],
+ "moji": "🔃"
+ },
+ "arrows_counterclockwise": {
+ "unicode": "1F504",
+ "unicode_alternates": [],
+ "name": "anticlockwise downwards and upwards open circle ar",
+ "shortname": ":arrows_counterclockwise:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "sync"],
+ "moji": "🔄"
+ },
+ "art": {
+ "unicode": "1F3A8",
+ "unicode_alternates": [],
+ "name": "artist palette",
+ "shortname": ":art:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["design", "draw", "paint", "artist", "palette", "art", "colors", "paint", "draw", "brush", "pastels", "oils"],
+ "moji": "🎨"
+ },
+ "articulated_lorry": {
+ "unicode": "1F69B",
+ "unicode_alternates": [],
+ "name": "articulated lorry",
+ "shortname": ":articulated_lorry:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cars", "transportation", "vehicle", "truck", "delivery", "semi", "lorry", "articulated"],
+ "moji": "🚛"
+ },
+ "ascending_notes": {
+ "unicode": "1F39C",
+ "unicode_alternates": [],
+ "name": "beamed ascending musical notes",
+ "shortname": ":ascending_notes:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["score", "music", "sound", "tone"]
+ },
+ "astonished": {
+ "unicode": "1F632",
+ "unicode_alternates": [],
+ "name": "astonished face",
+ "shortname": ":astonished:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "xox", "shocked", "surprise", "astonished"],
+ "moji": "😲"
+ },
+ "athletic_shoe": {
+ "unicode": "1F45F",
+ "unicode_alternates": [],
+ "name": "athletic shoe",
+ "shortname": ":athletic_shoe:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shoes", "sports"],
+ "moji": "👟"
+ },
+ "atm": {
+ "unicode": "1F3E7",
+ "unicode_alternates": [],
+ "name": "automated teller machine",
+ "shortname": ":atm:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["atm", "cash", "withdrawal", "money", "deposit", "financial", "bank", "adam", "payday", "bank", "blue-square", "cash", "money", "payment"],
+ "moji": "🏧"
+ },
+ "b": {
+ "unicode": "1F171",
+ "unicode_alternates": [],
+ "name": "negative squared latin capital letter b",
+ "shortname": ":b:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "letter", "red-square"],
+ "moji": "🅱"
+ },
+ "baby": {
+ "unicode": "1F476",
+ "unicode_alternates": [],
+ "name": "baby",
+ "shortname": ":baby:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["boy", "child", "infant"],
+ "moji": "👶"
+ },
+ "baby_bottle": {
+ "unicode": "1F37C",
+ "unicode_alternates": [],
+ "name": "baby bottle",
+ "shortname": ":baby_bottle:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["container", "food", "baby", "bottle", "milk", "mother", "nipple", "newborn", "formula"],
+ "moji": "🍼"
+ },
+ "baby_chick": {
+ "unicode": "1F424",
+ "unicode_alternates": [],
+ "name": "baby chick",
+ "shortname": ":baby_chick:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"],
+ "moji": "🐤"
+ },
+ "baby_symbol": {
+ "unicode": "1F6BC",
+ "unicode_alternates": [],
+ "name": "baby symbol",
+ "shortname": ":baby_symbol:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["child", "orange-square", "baby", "crawl", "newborn", "human", "diaper", "small", "babe"],
+ "moji": "🚼"
+ },
+ "back": {
+ "unicode": "1F519",
+ "unicode_alternates": [],
+ "name": "back with leftwards arrow above",
+ "shortname": ":back:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow"],
+ "moji": "🔙"
+ },
+ "baggage_claim": {
+ "unicode": "1F6C4",
+ "unicode_alternates": [],
+ "name": "baggage claim",
+ "shortname": ":baggage_claim:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["airport", "blue-square", "transport", "bag", "baggage", "luggage", "travel"],
+ "moji": "🛄"
+ },
+ "balloon": {
+ "unicode": "1F388",
+ "unicode_alternates": [],
+ "name": "balloon",
+ "shortname": ":balloon:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["celebration", "party", "balloon", "birthday", "celebration", "helium", "gas", "children", "float"],
+ "moji": "🎈"
+ },
+ "ballot_box": {
+ "unicode": "1F5F3",
+ "unicode_alternates": [],
+ "name": "ballot box with ballot",
+ "shortname": ":ballot_box:",
+ "category": "objects_symbols",
+ "aliases": [":ballot_box_with_ballot:"],
+ "aliases_ascii": [],
+ "keywords": ["vote"]
+ },
+ "ballot_box_check": {
+ "unicode": "1F5F9",
+ "unicode_alternates": [],
+ "name": "ballot box with bold check",
+ "shortname": ":ballot_box_check:",
+ "category": "objects_symbols",
+ "aliases": [":ballot_box_with_bold_check:"],
+ "aliases_ascii": [],
+ "keywords": ["mark", "vote"]
+ },
+ "ballot_box_with_check": {
+ "unicode": "2611",
+ "unicode_alternates": ["2611-FE0F"],
+ "name": "ballot box with check",
+ "shortname": ":ballot_box_with_check:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["agree", "ok"],
+ "moji": "☑"
+ },
+ "ballot_box_x": {
+ "unicode": "1F5F5",
+ "unicode_alternates": [],
+ "name": "ballot box with script x",
+ "shortname": ":ballot_box_x:",
+ "category": "objects_symbols",
+ "aliases": [":ballot_box_with_script_x:"],
+ "aliases_ascii": [],
+ "keywords": ["mark", "vote"]
+ },
+ "ballot_x": {
+ "unicode": "1F5F4",
+ "unicode_alternates": [],
+ "name": "ballot script x",
+ "shortname": ":ballot_x:",
+ "category": "objects_symbols",
+ "aliases": [":ballot_script_x:"],
+ "aliases_ascii": [],
+ "keywords": ["mark", "vote"]
+ },
+ "bamboo": {
+ "unicode": "1F38D",
+ "unicode_alternates": [],
+ "name": "pine decoration",
+ "shortname": ":bamboo:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "vegetable", "pine", "bamboo", "decoration", "new", "years", "spirits", "harvest", "prosperity", "longevity", "fortune", "luck", "welcome", "farming", "agriculture"],
+ "moji": "🎍"
+ },
+ "banana": {
+ "unicode": "1F34C",
+ "unicode_alternates": [],
+ "name": "banana",
+ "shortname": ":banana:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "banana", "peel", "bunch"],
+ "moji": "🍌"
+ },
+ "bangbang": {
+ "unicode": "203C",
+ "unicode_alternates": ["203C-FE0F"],
+ "name": "double exclamation mark",
+ "shortname": ":bangbang:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["exclamation", "surprise"],
+ "moji": "‼"
+ },
+ "bank": {
+ "unicode": "1F3E6",
+ "unicode_alternates": [],
+ "name": "bank",
+ "shortname": ":bank:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building"],
+ "moji": "🏦"
+ },
+ "bar_chart": {
+ "unicode": "1F4CA",
+ "unicode_alternates": [],
+ "name": "bar chart",
+ "shortname": ":bar_chart:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["graph", "presentation", "stats"],
+ "moji": "📊"
+ },
+ "barber": {
+ "unicode": "1F488",
+ "unicode_alternates": [],
+ "name": "barber pole",
+ "shortname": ":barber:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["hair", "salon", "style"],
+ "moji": "💈"
+ },
+ "baseball": {
+ "unicode": "26BE",
+ "unicode_alternates": ["26BE-FE0F"],
+ "name": "baseball",
+ "shortname": ":baseball:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["MLB", "balls", "sports"],
+ "moji": "⚾"
+ },
+ "basketball": {
+ "unicode": "1F3C0",
+ "unicode_alternates": [],
+ "name": "basketball and hoop",
+ "shortname": ":basketball:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["NBA", "balls", "sports", "basketball", "bball", "dribble", "hoop", "net", "swish", "rip city"],
+ "moji": "🏀"
+ },
+ "bath": {
+ "unicode": "1F6C0",
+ "unicode_alternates": [],
+ "name": "bath",
+ "shortname": ":bath:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"],
+ "moji": "🛀"
+ },
+ "bathtub": {
+ "unicode": "1F6C1",
+ "unicode_alternates": [],
+ "name": "bathtub",
+ "shortname": ":bathtub:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clean", "shower", "bath", "tub", "basin", "wash", "bubble", "soak", "bathroom", "soap", "water", "clean", "shampoo", "lather", "water"],
+ "moji": "🛁"
+ },
+ "battery": {
+ "unicode": "1F50B",
+ "unicode_alternates": [],
+ "name": "battery",
+ "shortname": ":battery:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["energy", "power", "sustain"],
+ "moji": "🔋"
+ },
+ "beach": {
+ "unicode": "1F3D6",
+ "unicode_alternates": [],
+ "name": "beach with umbrella",
+ "shortname": ":beach:",
+ "category": "travel_places",
+ "aliases": [":beach_with_umbrella:"],
+ "aliases_ascii": [],
+ "keywords": ["sand", "sun", "surf", "vacation", "relaxation", "tanning", "tan", "swimming"]
+ },
+ "bear": {
+ "unicode": "1F43B",
+ "unicode_alternates": [],
+ "name": "bear face",
+ "shortname": ":bear:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐻"
+ },
+ "bed": {
+ "unicode": "1F6CF",
+ "unicode_alternates": [],
+ "name": "bed",
+ "shortname": ":bed:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sleep", "sex", "queen", "full", "twin", "king", "mattress"]
+ },
+ "bee": {
+ "unicode": "1F41D",
+ "unicode_alternates": [],
+ "name": "honeybee",
+ "shortname": ":bee:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "insect", "bee", "queen", "buzz", "flower", "pollen", "sting", "honey", "hive", "bumble", "pollination"],
+ "moji": "🐝"
+ },
+ "beer": {
+ "unicode": "1F37A",
+ "unicode_alternates": [],
+ "name": "beer mug",
+ "shortname": ":beer:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "hops", "mug", "barley", "malt", "yeast", "portland", "oregon", "brewery", "micro", "pint", "boot"],
+ "moji": "🍺"
+ },
+ "beers": {
+ "unicode": "1F37B",
+ "unicode_alternates": [],
+ "name": "clinking beer mugs",
+ "shortname": ":beers:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beverage", "drink", "drunk", "party", "pub", "relax", "beer", "beers", "cheers", "mug", "toast", "celebrate", "pub", "bar", "jolly", "hops", "clink"],
+ "moji": "🍻"
+ },
+ "beetle": {
+ "unicode": "1F41E",
+ "unicode_alternates": [],
+ "name": "lady beetle",
+ "shortname": ":beetle:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["insect", "nature", "lady", "bug", "ladybug", "ladybird", "beetle", "cow", "lady cow", "insect", "endearment"],
+ "moji": "🐞"
+ },
+ "beginner": {
+ "unicode": "1F530",
+ "unicode_alternates": [],
+ "name": "japanese symbol for beginner",
+ "shortname": ":beginner:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["badge", "shield"],
+ "moji": "🔰"
+ },
+ "bell": {
+ "unicode": "1F514",
+ "unicode_alternates": [],
+ "name": "bell",
+ "shortname": ":bell:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chime", "christmas", "notification", "sound", "xmas"],
+ "moji": "🔔"
+ },
+ "bellhop": {
+ "unicode": "1F6CE",
+ "unicode_alternates": [],
+ "name": "bellhop bell",
+ "shortname": ":bellhop:",
+ "category": "travel_places",
+ "aliases": [":bellhop_bell:"],
+ "aliases_ascii": [],
+ "keywords": ["hotel", "porter", "ding"]
+ },
+ "bento": {
+ "unicode": "1F371",
+ "unicode_alternates": [],
+ "name": "bento box",
+ "shortname": ":bento:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["box", "food", "japanese", "bento", "japanese", "rice", "meal", "box", "obento", "convenient", "lunchbox"],
+ "moji": "🍱"
+ },
+ "bicyclist": {
+ "unicode": "1F6B4",
+ "unicode_alternates": [],
+ "name": "bicyclist",
+ "shortname": ":bicyclist:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bike", "exercise", "hipster", "sports", "bicyclist", "road", "bike", "pedal", "bicycle", "transportation"],
+ "moji": "🚴"
+ },
+ "bike": {
+ "unicode": "1F6B2",
+ "unicode_alternates": [],
+ "name": "bicycle",
+ "shortname": ":bike:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bicycle", "exercise", "hipster", "sports", "bike", "pedal", "bicycle", "transportation"],
+ "moji": "🚲"
+ },
+ "bikini": {
+ "unicode": "1F459",
+ "unicode_alternates": [],
+ "name": "bikini",
+ "shortname": ":bikini:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beach", "fashion", "female", "girl", "swimming", "woman"],
+ "moji": "👙"
+ },
+ "bird": {
+ "unicode": "1F426",
+ "unicode_alternates": [],
+ "name": "bird",
+ "shortname": ":bird:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "fly", "nature", "tweet"],
+ "moji": "🐦"
+ },
+ "birthday": {
+ "unicode": "1F382",
+ "unicode_alternates": [],
+ "name": "birthday cake",
+ "shortname": ":birthday:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cake", "party", "birthday", "birth", "cake", "dessert", "wish", "celebrate"],
+ "moji": "🎂"
+ },
+ "black_circle": {
+ "unicode": "26AB",
+ "unicode_alternates": ["26AB-FE0F"],
+ "name": "medium black circle",
+ "shortname": ":black_circle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "⚫"
+ },
+ "black_joker": {
+ "unicode": "1F0CF",
+ "unicode_alternates": [],
+ "name": "playing card black joker",
+ "shortname": ":black_joker:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cards", "game", "poker"],
+ "moji": "🃏"
+ },
+ "black_large_square": {
+ "unicode": "2B1B",
+ "unicode_alternates": ["2B1B-FE0F"],
+ "name": "black large square",
+ "shortname": ":black_large_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "⬛"
+ },
+ "black_medium_small_square": {
+ "unicode": "25FE",
+ "unicode_alternates": ["25FE-FE0F"],
+ "name": "black medium small square",
+ "shortname": ":black_medium_small_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "◾"
+ },
+ "black_medium_square": {
+ "unicode": "25FC",
+ "unicode_alternates": ["25FC-FE0F"],
+ "name": "black medium square",
+ "shortname": ":black_medium_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "◼"
+ },
+ "black_nib": {
+ "unicode": "2712",
+ "unicode_alternates": ["2712-FE0F"],
+ "name": "black nib",
+ "shortname": ":black_nib:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["pen", "stationery"],
+ "moji": "✒"
+ },
+ "black_small_square": {
+ "unicode": "25AA",
+ "unicode_alternates": ["25AA-FE0F"],
+ "name": "black small square",
+ "shortname": ":black_small_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "▪"
+ },
+ "black_square_button": {
+ "unicode": "1F532",
+ "unicode_alternates": [],
+ "name": "black square button",
+ "shortname": ":black_square_button:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["frame"],
+ "moji": "🔲"
+ },
+ "blossom": {
+ "unicode": "1F33C",
+ "unicode_alternates": [],
+ "name": "blossom",
+ "shortname": ":blossom:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flowers", "nature", "yellow", "blossom", "daisy", "flower"],
+ "moji": "🌼"
+ },
+ "blowfish": {
+ "unicode": "1F421",
+ "unicode_alternates": [],
+ "name": "blowfish",
+ "shortname": ":blowfish:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "nature", "ocean", "sea", "blowfish", "pufferfish", "puffer", "ballonfish", "toadfish", "fugu fish", "sushi"],
+ "moji": "🐡"
+ },
+ "blue_book": {
+ "unicode": "1F4D8",
+ "unicode_alternates": [],
+ "name": "blue book",
+ "shortname": ":blue_book:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["knowledge", "library", "read"],
+ "moji": "📘"
+ },
+ "blue_car": {
+ "unicode": "1F699",
+ "unicode_alternates": [],
+ "name": "recreational vehicle",
+ "shortname": ":blue_car:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["car", "suv", "car", "wagon", "automobile"],
+ "moji": "🚙"
+ },
+ "blue_heart": {
+ "unicode": "1F499",
+ "unicode_alternates": [],
+ "name": "blue heart",
+ "shortname": ":blue_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines", "blue", "heart", "love", "stability", "truth", "loyalty", "trust"],
+ "moji": "💙"
+ },
+ "blush": {
+ "unicode": "1F60A",
+ "unicode_alternates": [],
+ "name": "smiling face with smiling eyes",
+ "shortname": ":blush:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["crush", "embarrassed", "face", "flushed", "happy", "shy", "smile", "smiling", "smile", "smiley"],
+ "moji": "😊"
+ },
+ "boar": {
+ "unicode": "1F417",
+ "unicode_alternates": [],
+ "name": "boar",
+ "shortname": ":boar:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐗"
+ },
+ "bomb": {
+ "unicode": "1F4A3",
+ "unicode_alternates": [],
+ "name": "bomb",
+ "shortname": ":bomb:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["boom", "explode"],
+ "moji": "💣"
+ },
+ "book": {
+ "unicode": "1F4D6",
+ "unicode_alternates": [],
+ "name": "open book",
+ "shortname": ":book:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["library", "literature"],
+ "moji": "📖"
+ },
+ "book2": {
+ "unicode": "1F56E",
+ "unicode_alternates": [],
+ "name": "book",
+ "shortname": ":book2:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["library", "literature", "novel", "reading", "story"]
+ },
+ "bookmark": {
+ "unicode": "1F516",
+ "unicode_alternates": [],
+ "name": "bookmark",
+ "shortname": ":bookmark:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["favorite"],
+ "moji": "🔖"
+ },
+ "bookmark_tabs": {
+ "unicode": "1F4D1",
+ "unicode_alternates": [],
+ "name": "bookmark tabs",
+ "shortname": ":bookmark_tabs:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["favorite"],
+ "moji": "📑"
+ },
+ "books": {
+ "unicode": "1F4DA",
+ "unicode_alternates": [],
+ "name": "books",
+ "shortname": ":books:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["library", "literature"],
+ "moji": "📚"
+ },
+ "boom": {
+ "unicode": "1F4A5",
+ "unicode_alternates": [],
+ "name": "collision symbol",
+ "shortname": ":boom:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bomb", "explode", "explosion", "boom", "bang", "collision", "fire", "emphasis", "wow", "bam"],
+ "moji": "💥"
+ },
+ "boot": {
+ "unicode": "1F462",
+ "unicode_alternates": [],
+ "name": "womans boots",
+ "shortname": ":boot:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "shoes"],
+ "moji": "👢"
+ },
+ "bouquet": {
+ "unicode": "1F490",
+ "unicode_alternates": [],
+ "name": "bouquet",
+ "shortname": ":bouquet:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flowers", "nature"],
+ "moji": "💐"
+ },
+ "bouquet2": {
+ "unicode": "1F395",
+ "unicode_alternates": [],
+ "name": "bouquet of flowers",
+ "shortname": ":bouquet2:",
+ "category": "celebration",
+ "aliases": [":bouquet_of_flowers:"],
+ "aliases_ascii": [],
+ "keywords": ["nature", "marriage", "wedding", "bride"]
+ },
+ "bow": {
+ "unicode": "1F647",
+ "unicode_alternates": [],
+ "name": "person bowing deeply",
+ "shortname": ":bow:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["boy", "male", "man", "sorry", "bow", "respect", "curtsy", "bend"],
+ "moji": "🙇"
+ },
+ "bowling": {
+ "unicode": "1F3B3",
+ "unicode_alternates": [],
+ "name": "bowling",
+ "shortname": ":bowling:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fun", "play", "sports", "bowl", "bowling", "ball", "pin", "strike", "spare", "game"],
+ "moji": "🎳"
+ },
+ "boy": {
+ "unicode": "1F466",
+ "unicode_alternates": [],
+ "name": "boy",
+ "shortname": ":boy:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["guy", "male", "man"],
+ "moji": "👦"
+ },
+ "boys_symbol": {
+ "unicode": "1F6C9",
+ "unicode_alternates": [],
+ "name": "boys symbol",
+ "shortname": ":boys_symbol:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["male", "child"]
+ },
+ "bread": {
+ "unicode": "1F35E",
+ "unicode_alternates": [],
+ "name": "bread",
+ "shortname": ":bread:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["breakfast", "food", "toast", "wheat", "bread", "loaf", "yeast"],
+ "moji": "🍞"
+ },
+ "bride_with_veil": {
+ "unicode": "1F470",
+ "unicode_alternates": [],
+ "name": "bride with veil",
+ "shortname": ":bride_with_veil:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["couple", "marriage", "wedding", "bride", "wedding", "planning", "veil", "gown", "dress", "engagement", "white"],
+ "moji": "👰"
+ },
+ "bridge_at_night": {
+ "unicode": "1F309",
+ "unicode_alternates": [],
+ "name": "bridge at night",
+ "shortname": ":bridge_at_night:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo", "sanfrancisco", "bridge", "night", "water", "road", "evening", "suspension", "golden", "gate"],
+ "moji": "🌉"
+ },
+ "briefcase": {
+ "unicode": "1F4BC",
+ "unicode_alternates": [],
+ "name": "briefcase",
+ "shortname": ":briefcase:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["business", "documents", "work"],
+ "moji": "💼"
+ },
+ "broken_heart": {
+ "unicode": "1F494",
+ "unicode_alternates": [],
+ "name": "broken heart",
+ "shortname": ":broken_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["</3"],
+ "keywords": ["sad", "sorry"],
+ "moji": "💔"
+ },
+ "bug": {
+ "unicode": "1F41B",
+ "unicode_alternates": [],
+ "name": "bug",
+ "shortname": ":bug:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["insect", "nature", "bug", "insect", "virus", "error"],
+ "moji": "🐛"
+ },
+ "bulb": {
+ "unicode": "1F4A1",
+ "unicode_alternates": [],
+ "name": "electric light bulb",
+ "shortname": ":bulb:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["electricity", "light", "idea", "bulb", "light"],
+ "moji": "💡"
+ },
+ "bullettrain_front": {
+ "unicode": "1F685",
+ "unicode_alternates": [],
+ "name": "high-speed train with bullet nose",
+ "shortname": ":bullettrain_front:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "train", "bullet", "rail"],
+ "moji": "🚅"
+ },
+ "bullettrain_side": {
+ "unicode": "1F684",
+ "unicode_alternates": [],
+ "name": "high-speed train",
+ "shortname": ":bullettrain_side:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "train", "bullet", "rail"],
+ "moji": "🚄"
+ },
+ "bullhorn": {
+ "unicode": "1F56B",
+ "unicode_alternates": [],
+ "name": "bullhorn",
+ "shortname": ":bullhorn:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sound", "noise", "announcement", "megaphone"]
+ },
+ "bullhorn_waves": {
+ "unicode": "1F56C",
+ "unicode_alternates": [],
+ "name": "bullhorn with sound waves",
+ "shortname": ":bullhorn_waves:",
+ "category": "objects_symbols",
+ "aliases": [":bullhorn_with_sound_waves:"],
+ "aliases_ascii": [],
+ "keywords": ["sound", "noise", "announcement", "megaphone"]
+ },
+ "bus": {
+ "unicode": "1F68C",
+ "unicode_alternates": [],
+ "name": "bus",
+ "shortname": ":bus:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["car", "transportation", "vehicle", "bus", "school", "city", "transportation", "public"],
+ "moji": "🚌"
+ },
+ "busstop": {
+ "unicode": "1F68F",
+ "unicode_alternates": [],
+ "name": "bus stop",
+ "shortname": ":busstop:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "bus", "stop", "city", "transport", "transportation"],
+ "moji": "🚏"
+ },
+ "bust_in_silhouette": {
+ "unicode": "1F464",
+ "unicode_alternates": [],
+ "name": "bust in silhouette",
+ "shortname": ":bust_in_silhouette:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["human", "man", "person", "user", "silhouette", "person", "user", "member", "account", "guest", "icon", "avatar", "profile", "me", "myself", "i"],
+ "moji": "👤"
+ },
+ "busts_in_silhouette": {
+ "unicode": "1F465",
+ "unicode_alternates": [],
+ "name": "busts in silhouette",
+ "shortname": ":busts_in_silhouette:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["group", "human", "man", "person", "team", "user", "silhouette", "silhouettes", "people", "user", "members", "accounts", "relationship", "shadow"],
+ "moji": "👥"
+ },
+ "cactus": {
+ "unicode": "1F335",
+ "unicode_alternates": [],
+ "name": "cactus",
+ "shortname": ":cactus:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "vegetable", "cactus", "desert", "drought", "spike", "poke"],
+ "moji": "🌵"
+ },
+ "cake": {
+ "unicode": "1F370",
+ "unicode_alternates": [],
+ "name": "shortcake",
+ "shortname": ":cake:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "food", "cake", "short", "dessert", "strawberry"],
+ "moji": "🍰"
+ },
+ "calculator": {
+ "unicode": "1F5A9",
+ "unicode_alternates": [],
+ "name": "pocket calculator",
+ "shortname": ":calculator:",
+ "category": "objects_symbols",
+ "aliases": [":pocket calculator:"],
+ "aliases_ascii": [],
+ "keywords": ["add", "subtract", "multiple", "divide", "scientific"]
+ },
+ "calendar": {
+ "unicode": "1F4C6",
+ "unicode_alternates": [],
+ "name": "tear-off calendar",
+ "shortname": ":calendar:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["schedule"],
+ "moji": "📆"
+ },
+ "calendar_spiral": {
+ "unicode": "1F5D3",
+ "unicode_alternates": [],
+ "name": "spiral calendar pad",
+ "shortname": ":calendar_spiral:",
+ "category": "objects_symbols",
+ "aliases": [":spiral_calendar_pad:"],
+ "aliases_ascii": [],
+ "keywords": ["schedule", "date", "day"]
+ },
+ "calling": {
+ "unicode": "1F4F2",
+ "unicode_alternates": [],
+ "name": "mobile phone with rightwards arrow at left",
+ "shortname": ":calling:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["incoming", "iphone"],
+ "moji": "📲"
+ },
+ "camel": {
+ "unicode": "1F42B",
+ "unicode_alternates": [],
+ "name": "bactrian camel",
+ "shortname": ":camel:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "hot", "nature", "bactrian", "camel", "hump", "desert", "central asia", "heat", "hot", "water", "hump day", "wednesday", "sex"],
+ "moji": "🐫"
+ },
+ "camera": {
+ "unicode": "1F4F7",
+ "unicode_alternates": [],
+ "name": "camera",
+ "shortname": ":camera:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["gadgets", "photo"],
+ "moji": "📷"
+ },
+ "camera_with_flash": {
+ "unicode": "1F4F8",
+ "unicode_alternates": [],
+ "name": "camera with flash",
+ "shortname": ":camera_with_flash:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo", "picture"]
+ },
+ "camping": {
+ "unicode": "1F3D5",
+ "unicode_alternates": [],
+ "name": "camping",
+ "shortname": ":camping:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["outdoors", "nature", "wilderness", "roughing", "activity"]
+ },
+ "cancellation_x": {
+ "unicode": "1F5D9",
+ "unicode_alternates": [],
+ "name": "cancellation x",
+ "shortname": ":cancellation_x:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cancel", "stop", "delete"]
+ },
+ "cancer": {
+ "unicode": "264B",
+ "unicode_alternates": ["264B-FE0F"],
+ "name": "cancer",
+ "shortname": ":cancer:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cancer", "crab", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "moji": "♋"
+ },
+ "candle": {
+ "unicode": "1F56F",
+ "unicode_alternates": [],
+ "name": "candle",
+ "shortname": ":candle:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["light", "wax"]
+ },
+ "candy": {
+ "unicode": "1F36C",
+ "unicode_alternates": [],
+ "name": "candy",
+ "shortname": ":candy:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "snack", "candy", "sugar", "sweet", "hard"],
+ "moji": "🍬"
+ },
+ "capital_abcd": {
+ "unicode": "1F520",
+ "unicode_alternates": [],
+ "name": "input symbol for latin capital letters",
+ "shortname": ":capital_abcd:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "blue-square", "words"],
+ "moji": "🔠"
+ },
+ "capricorn": {
+ "unicode": "2651",
+ "unicode_alternates": ["2651-FE0F"],
+ "name": "capricorn",
+ "shortname": ":capricorn:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["capricorn", "sea-goat", "goat-horned", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "moji": "♑"
+ },
+ "card_box": {
+ "unicode": "1F5C3",
+ "unicode_alternates": [],
+ "name": "card file box",
+ "shortname": ":card_box:",
+ "category": "objects_symbols",
+ "aliases": [":card_file_box:"],
+ "aliases_ascii": [],
+ "keywords": ["index", "organization"]
+ },
+ "card_index": {
+ "unicode": "1F4C7",
+ "unicode_alternates": [],
+ "name": "card index",
+ "shortname": ":card_index:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["business", "stationery"],
+ "moji": "📇"
+ },
+ "carousel_horse": {
+ "unicode": "1F3A0",
+ "unicode_alternates": [],
+ "name": "carousel horse",
+ "shortname": ":carousel_horse:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["carnival", "horse", "photo", "carousel", "horse", "amusement", "park", "ride", "entertainment", "park", "fair"],
+ "moji": "🎠"
+ },
+ "cartridge": {
+ "unicode": "1F5AD",
+ "unicode_alternates": [],
+ "name": "tape cartridge",
+ "shortname": ":cartridge:",
+ "category": "objects_symbols",
+ "aliases": [":tape_cartridge:"],
+ "aliases_ascii": [],
+ "keywords": ["oldschool", "save", "technology", "disk", "storage", "information", "computer", "drive", "megabyte"]
+ },
+ "cat": {
+ "unicode": "1F431",
+ "unicode_alternates": [],
+ "name": "cat face",
+ "shortname": ":cat:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "meow"],
+ "moji": "🐱"
+ },
+ "cat2": {
+ "unicode": "1F408",
+ "unicode_alternates": [],
+ "name": "cat",
+ "shortname": ":cat2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "meow", "pet", "cat", "kitten", "meow"],
+ "moji": "🐈"
+ },
+ "celtic_cross": {
+ "unicode": "1F548",
+ "unicode_alternates": [],
+ "name": "celtic cross",
+ "shortname": ":celtic_cross:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["religion", "symbol"]
+ },
+ "chart": {
+ "unicode": "1F4B9",
+ "unicode_alternates": [],
+ "name": "chart with upwards trend and yen sign",
+ "shortname": ":chart:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["graph", "green-square"],
+ "moji": "💹"
+ },
+ "chart_with_downwards_trend": {
+ "unicode": "1F4C9",
+ "unicode_alternates": [],
+ "name": "chart with downwards trend",
+ "shortname": ":chart_with_downwards_trend:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["graph"],
+ "moji": "📉"
+ },
+ "chart_with_upwards_trend": {
+ "unicode": "1F4C8",
+ "unicode_alternates": [],
+ "name": "chart with upwards trend",
+ "shortname": ":chart_with_upwards_trend:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["graph"],
+ "moji": "📈"
+ },
+ "checkered_flag": {
+ "unicode": "1F3C1",
+ "unicode_alternates": [],
+ "name": "chequered flag",
+ "shortname": ":checkered_flag:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["contest", "finishline", "gokart", "rase", "checkered", "chequred", "race", "flag", "finish", "complete", "end"],
+ "moji": "🏁"
+ },
+ "cherries": {
+ "unicode": "1F352",
+ "unicode_alternates": [],
+ "name": "cherries",
+ "shortname": ":cherries:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "cherry", "cherries", "tree", "fruit", "pit"],
+ "moji": "🍒"
+ },
+ "cherry_blossom": {
+ "unicode": "1F338",
+ "unicode_alternates": [],
+ "name": "cherry blossom",
+ "shortname": ":cherry_blossom:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flower", "nature", "plant", "cherry", "blossom", "tree", "flower"],
+ "moji": "🌸"
+ },
+ "chestnut": {
+ "unicode": "1F330",
+ "unicode_alternates": [],
+ "name": "chestnut",
+ "shortname": ":chestnut:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "squirrel", "chestnut", "roasted", "food", "tree"],
+ "moji": "🌰"
+ },
+ "chicken": {
+ "unicode": "1F414",
+ "unicode_alternates": [],
+ "name": "chicken",
+ "shortname": ":chicken:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cluck", "chicken", "hen", "poultry", "livestock"],
+ "moji": "🐔"
+ },
+ "children_crossing": {
+ "unicode": "1F6B8",
+ "unicode_alternates": [],
+ "name": "children crossing",
+ "shortname": ":children_crossing:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["school", "children", "kids", "caution", "crossing", "street", "crosswalk", "slow"],
+ "moji": "🚸"
+ },
+ "chipmunk": {
+ "unicode": "1F43F",
+ "unicode_alternates": [],
+ "name": "chipmunk",
+ "shortname": ":chipmunk:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"]
+ },
+ "chocolate_bar": {
+ "unicode": "1F36B",
+ "unicode_alternates": [],
+ "name": "chocolate bar",
+ "shortname": ":chocolate_bar:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "food", "snack", "chocolate", "bar", "candy", "coca", "hershey&#039;s"],
+ "moji": "🍫"
+ },
+ "christmas_tree": {
+ "unicode": "1F384",
+ "unicode_alternates": [],
+ "name": "christmas tree",
+ "shortname": ":christmas_tree:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["celebration", "december", "festival", "vacation", "xmas", "christmas", "xmas", "santa", "holiday", "winter", "december", "santa", "evergreen", "ornaments", "jesus", "gifts", "presents"],
+ "moji": "🎄"
+ },
+ "church": {
+ "unicode": "26EA",
+ "unicode_alternates": ["26EA-FE0F"],
+ "name": "church",
+ "shortname": ":church:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "christ", "religion"],
+ "moji": "⛪"
+ },
+ "cinema": {
+ "unicode": "1F3A6",
+ "unicode_alternates": [],
+ "name": "cinema",
+ "shortname": ":cinema:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "film", "movie", "record", "cinema", "movie", "theater", "motion", "picture"],
+ "moji": "🎦"
+ },
+ "circus_tent": {
+ "unicode": "1F3AA",
+ "unicode_alternates": [],
+ "name": "circus tent",
+ "shortname": ":circus_tent:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["carnival", "festival", "party", "circus", "tent", "event", "carnival", "big", "top", "canvas"],
+ "moji": "🎪"
+ },
+ "city_dusk": {
+ "unicode": "1F306",
+ "unicode_alternates": [],
+ "name": "cityscape at dusk",
+ "shortname": ":city_dusk:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo", "city", "scape", "sunset", "dusk", "lights", "evening", "metropolitan", "night", "dark"],
+ "moji": "🌆"
+ },
+ "city_sunset": {
+ "unicode": "1F307",
+ "unicode_alternates": [],
+ "name": "sunset over buildings",
+ "shortname": ":city_sunset:",
+ "category": "places",
+ "aliases": [":city_sunrise:"],
+ "aliases_ascii": [],
+ "keywords": ["photo", "city", "scape", "sunrise", "dawn", "light", "morning", "metropolitan", "rise", "sun"],
+ "moji": "🌇"
+ },
+ "cityscape": {
+ "unicode": "1F3D9",
+ "unicode_alternates": [],
+ "name": "cityscape",
+ "shortname": ":cityscape:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["skyscraper", "city", "view", "lights", "buiildings", "metropolis"]
+ },
+ "clap": {
+ "unicode": "1F44F",
+ "unicode_alternates": [],
+ "name": "clapping hands sign",
+ "shortname": ":clap:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["applause", "congrats", "hands", "praise", "clapping", "appreciation", "approval", "sound", "encouragement", "enthusiasm"],
+ "moji": "👏"
+ },
+ "clapper": {
+ "unicode": "1F3AC",
+ "unicode_alternates": [],
+ "name": "clapper board",
+ "shortname": ":clapper:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["film", "movie", "record", "clapper", "board", "clapboard", "movie", "film", "take"],
+ "moji": "🎬"
+ },
+ "classical_building": {
+ "unicode": "1F3DB",
+ "unicode_alternates": [],
+ "name": "classical building",
+ "shortname": ":classical_building:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["government", "architecture", "history", "iconic", "genre"]
+ },
+ "clipboard": {
+ "unicode": "1F4CB",
+ "unicode_alternates": [],
+ "name": "clipboard",
+ "shortname": ":clipboard:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents", "stationery"],
+ "moji": "📋"
+ },
+ "clock": {
+ "unicode": "1F570",
+ "unicode_alternates": [],
+ "name": "mantlepiece clock",
+ "shortname": ":clock:",
+ "category": "objects_symbols",
+ "aliases": [":mantlepiece_clock:"],
+ "aliases_ascii": [],
+ "keywords": ["time"]
+ },
+ "clock1": {
+ "unicode": "1F550",
+ "unicode_alternates": [],
+ "name": "clock face one oclock",
+ "shortname": ":clock1:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕐"
+ },
+ "clock10": {
+ "unicode": "1F559",
+ "unicode_alternates": [],
+ "name": "clock face ten oclock",
+ "shortname": ":clock10:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕙"
+ },
+ "clock1030": {
+ "unicode": "1F565",
+ "unicode_alternates": [],
+ "name": "clock face ten-thirty",
+ "shortname": ":clock1030:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕥"
+ },
+ "clock11": {
+ "unicode": "1F55A",
+ "unicode_alternates": [],
+ "name": "clock face eleven oclock",
+ "shortname": ":clock11:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕚"
+ },
+ "clock1130": {
+ "unicode": "1F566",
+ "unicode_alternates": [],
+ "name": "clock face eleven-thirty",
+ "shortname": ":clock1130:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕦"
+ },
+ "clock12": {
+ "unicode": "1F55B",
+ "unicode_alternates": [],
+ "name": "clock face twelve oclock",
+ "shortname": ":clock12:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕛"
+ },
+ "clock1230": {
+ "unicode": "1F567",
+ "unicode_alternates": [],
+ "name": "clock face twelve-thirty",
+ "shortname": ":clock1230:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"]
+ },
+ "clock130": {
+ "unicode": "1F55C",
+ "unicode_alternates": [],
+ "name": "clock face one-thirty",
+ "shortname": ":clock130:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕜"
+ },
+ "clock2": {
+ "unicode": "1F551",
+ "unicode_alternates": [],
+ "name": "clock face two oclock",
+ "shortname": ":clock2:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕑"
+ },
+ "clock230": {
+ "unicode": "1F55D",
+ "unicode_alternates": [],
+ "name": "clock face two-thirty",
+ "shortname": ":clock230:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕝"
+ },
+ "clock3": {
+ "unicode": "1F552",
+ "unicode_alternates": [],
+ "name": "clock face three oclock",
+ "shortname": ":clock3:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕒"
+ },
+ "clock330": {
+ "unicode": "1F55E",
+ "unicode_alternates": [],
+ "name": "clock face three-thirty",
+ "shortname": ":clock330:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕞"
+ },
+ "clock4": {
+ "unicode": "1F553",
+ "unicode_alternates": [],
+ "name": "clock face four oclock",
+ "shortname": ":clock4:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕓"
+ },
+ "clock430": {
+ "unicode": "1F55F",
+ "unicode_alternates": [],
+ "name": "clock face four-thirty",
+ "shortname": ":clock430:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕟"
+ },
+ "clock5": {
+ "unicode": "1F554",
+ "unicode_alternates": [],
+ "name": "clock face five oclock",
+ "shortname": ":clock5:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕔"
+ },
+ "clock530": {
+ "unicode": "1F560",
+ "unicode_alternates": [],
+ "name": "clock face five-thirty",
+ "shortname": ":clock530:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕠"
+ },
+ "clock6": {
+ "unicode": "1F555",
+ "unicode_alternates": [],
+ "name": "clock face six oclock",
+ "shortname": ":clock6:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕕"
+ },
+ "clock630": {
+ "unicode": "1F561",
+ "unicode_alternates": [],
+ "name": "clock face six-thirty",
+ "shortname": ":clock630:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕡"
+ },
+ "clock7": {
+ "unicode": "1F556",
+ "unicode_alternates": [],
+ "name": "clock face seven oclock",
+ "shortname": ":clock7:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕖"
+ },
+ "clock730": {
+ "unicode": "1F562",
+ "unicode_alternates": [],
+ "name": "clock face seven-thirty",
+ "shortname": ":clock730:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕢"
+ },
+ "clock8": {
+ "unicode": "1F557",
+ "unicode_alternates": [],
+ "name": "clock face eight oclock",
+ "shortname": ":clock8:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕗"
+ },
+ "clock830": {
+ "unicode": "1F563",
+ "unicode_alternates": [],
+ "name": "clock face eight-thirty",
+ "shortname": ":clock830:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕣"
+ },
+ "clock9": {
+ "unicode": "1F558",
+ "unicode_alternates": [],
+ "name": "clock face nine oclock",
+ "shortname": ":clock9:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕘"
+ },
+ "clock930": {
+ "unicode": "1F564",
+ "unicode_alternates": [],
+ "name": "clock face nine-thirty",
+ "shortname": ":clock930:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "time"],
+ "moji": "🕤"
+ },
+ "clockwise_arrows": {
+ "unicode": "1F5D8",
+ "unicode_alternates": [],
+ "name": "clockwise right and left semicircle arrows",
+ "shortname": ":clockwise_arrows:",
+ "category": "objects_symbols",
+ "aliases": [":clockwise_right_and_left_semicircle_arrows:"],
+ "aliases_ascii": [],
+ "keywords": ["sync"]
+ },
+ "closed_book": {
+ "unicode": "1F4D5",
+ "unicode_alternates": [],
+ "name": "closed book",
+ "shortname": ":closed_book:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["knowledge", "library", "read"],
+ "moji": "📕"
+ },
+ "closed_lock_with_key": {
+ "unicode": "1F510",
+ "unicode_alternates": [],
+ "name": "closed lock with key",
+ "shortname": ":closed_lock_with_key:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["privacy", "security"],
+ "moji": "🔐"
+ },
+ "closed_umbrella": {
+ "unicode": "1F302",
+ "unicode_alternates": [],
+ "name": "closed umbrella",
+ "shortname": ":closed_umbrella:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["drizzle", "rain", "weather", "umbrella", "closed", "rain", "moisture", "protection", "sun", "ultraviolet", "uv"],
+ "moji": "🌂"
+ },
+ "cloud": {
+ "unicode": "2601",
+ "unicode_alternates": ["2601-FE0F"],
+ "name": "cloud",
+ "shortname": ":cloud:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sky", "weather"],
+ "moji": "☁"
+ },
+ "cloud_lightning": {
+ "unicode": "1F329",
+ "unicode_alternates": [],
+ "name": "cloud with lightning",
+ "shortname": ":cloud_lightning:",
+ "category": "nature",
+ "aliases": [":cloud_with_lightning:"],
+ "aliases_ascii": [],
+ "keywords": ["weather", "thunder"]
+ },
+ "cloud_rain": {
+ "unicode": "1F327",
+ "unicode_alternates": [],
+ "name": "cloud with rain",
+ "shortname": ":cloud_rain:",
+ "category": "nature",
+ "aliases": [":cloud_with_rain:"],
+ "aliases_ascii": [],
+ "keywords": ["weather", "wet"]
+ },
+ "cloud_snow": {
+ "unicode": "1F328",
+ "unicode_alternates": [],
+ "name": "cloud with snow",
+ "shortname": ":cloud_snow:",
+ "category": "nature",
+ "aliases": [":cloud_with_snow:"],
+ "aliases_ascii": [],
+ "keywords": ["weather", "cold"]
+ },
+ "cloud_tornado": {
+ "unicode": "1F32A",
+ "unicode_alternates": [],
+ "name": "cloud with tornado",
+ "shortname": ":cloud_tornado:",
+ "category": "nature",
+ "aliases": [":cloud_with_tornado:"],
+ "aliases_ascii": [],
+ "keywords": ["weather", "destruction", "funnel"]
+ },
+ "clubs": {
+ "unicode": "2663",
+ "unicode_alternates": ["2663-FE0F"],
+ "name": "black club suit",
+ "shortname": ":clubs:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cards", "poker"],
+ "moji": "♣"
+ },
+ "cocktail": {
+ "unicode": "1F378",
+ "unicode_alternates": [],
+ "name": "cocktail glass",
+ "shortname": ":cocktail:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alcohol", "beverage", "drink", "drunk", "cocktail", "mixed", "drink", "alcohol", "glass", "martini", "bar"],
+ "moji": "🍸"
+ },
+ "coffee": {
+ "unicode": "2615",
+ "unicode_alternates": ["2615-FE0F"],
+ "name": "hot beverage",
+ "shortname": ":coffee:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beverage", "cafe", "drink", "espresso"],
+ "moji": "☕"
+ },
+ "cold_sweat": {
+ "unicode": "1F630",
+ "unicode_alternates": [],
+ "name": "face with open mouth and cold sweat",
+ "shortname": ":cold_sweat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "nervous", "sweat", "exasperated", "frustrated"],
+ "moji": "😰"
+ },
+ "compression": {
+ "unicode": "1F5DC",
+ "unicode_alternates": [],
+ "name": "compression",
+ "shortname": ":compression:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["reduce"]
+ },
+ "computer": {
+ "unicode": "1F4BB",
+ "unicode_alternates": [],
+ "name": "personal computer",
+ "shortname": ":computer:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["laptop", "tech"],
+ "moji": "💻"
+ },
+ "computer_old": {
+ "unicode": "1F5B3",
+ "unicode_alternates": [],
+ "name": "old personal computer",
+ "shortname": ":computer_old:",
+ "category": "objects_symbols",
+ "aliases": [":old_personal_computer:"],
+ "aliases_ascii": [],
+ "keywords": ["cpu", "terminal"]
+ },
+ "confetti_ball": {
+ "unicode": "1F38A",
+ "unicode_alternates": [],
+ "name": "confetti ball",
+ "shortname": ":confetti_ball:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["festival", "party", "party", "congratulations", "confetti", "ball", "celebrate", "win", "birthday", "new years", "wedding"],
+ "moji": "🎊"
+ },
+ "confounded": {
+ "unicode": "1F616",
+ "unicode_alternates": [],
+ "name": "confounded face",
+ "shortname": ":confounded:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["confused", "face", "sick", "unwell", "confound", "amaze", "perplex", "puzzle", "mystify"],
+ "moji": "😖"
+ },
+ "confused": {
+ "unicode": "1F615",
+ "unicode_alternates": [],
+ "name": "confused face",
+ "shortname": ":confused:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [">:\\", ">:/", ":-/", ":-.", ":/", ":\\", "=/", "=\\", ":L", "=L"],
+ "keywords": ["confused", "confuse", "daze", "perplex", "puzzle", "indifference", "skeptical", "undecided", "uneasy", "hesitant"],
+ "moji": "😕"
+ },
+ "congratulations": {
+ "unicode": "3297",
+ "unicode_alternates": ["3297-FE0F"],
+ "name": "circled ideograph congratulation",
+ "shortname": ":congratulations:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "japanese", "kanji"],
+ "moji": "㊗"
+ },
+ "construction": {
+ "unicode": "1F6A7",
+ "unicode_alternates": [],
+ "name": "construction sign",
+ "shortname": ":construction:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["caution", "progress", "wip"],
+ "moji": "🚧"
+ },
+ "construction_worker": {
+ "unicode": "1F477",
+ "unicode_alternates": [],
+ "name": "construction worker",
+ "shortname": ":construction_worker:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["human", "male", "man", "wip"],
+ "moji": "👷"
+ },
+ "control_knobs": {
+ "unicode": "1F39B",
+ "unicode_alternates": [],
+ "name": "control knobs",
+ "shortname": ":control_knobs:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dial"]
+ },
+ "contruction_site": {
+ "unicode": "1F3D7",
+ "unicode_alternates": [],
+ "name": "building construction",
+ "shortname": ":contruction_site:",
+ "category": "travel_places",
+ "aliases": [":building_construction:"],
+ "aliases_ascii": [],
+ "keywords": ["site", "work"]
+ },
+ "convenience_store": {
+ "unicode": "1F3EA",
+ "unicode_alternates": [],
+ "name": "convenience store",
+ "shortname": ":convenience_store:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building"],
+ "moji": "🏪"
+ },
+ "cookie": {
+ "unicode": "1F36A",
+ "unicode_alternates": [],
+ "name": "cookie",
+ "shortname": ":cookie:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chocolate", "food", "oreo", "snack", "cookie", "dessert", "biscuit", "sweet", "chocolate"],
+ "moji": "🍪"
+ },
+ "cool": {
+ "unicode": "1F192",
+ "unicode_alternates": [],
+ "name": "squared cool",
+ "shortname": ":cool:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "words"],
+ "moji": "🆒"
+ },
+ "cop": {
+ "unicode": "1F46E",
+ "unicode_alternates": [],
+ "name": "police officer",
+ "shortname": ":cop:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrest", "enforcement", "law", "man", "police"],
+ "moji": "👮"
+ },
+ "copyright": {
+ "moji": "©",
+ "unicode": "00A9",
+ "unicode_alternates": [],
+ "name": "copyright sign",
+ "shortname": ":copyright:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ip", "license"]
+ },
+ "corn": {
+ "unicode": "1F33D",
+ "unicode_alternates": [],
+ "name": "ear of maize",
+ "shortname": ":corn:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "plant", "vegetable", "corn", "maize", "food", "iowa", "kernel", "popcorn", "husk", "yellow", "stalk", "cob", "ear"],
+ "moji": "🌽"
+ },
+ "couch": {
+ "unicode": "1F6CB",
+ "unicode_alternates": [],
+ "name": "couch and lamp",
+ "shortname": ":couch:",
+ "category": "travel_places",
+ "aliases": [":couch_and_lamp:"],
+ "aliases_ascii": [],
+ "keywords": ["lounge", "sectional", "sofa", "loveseat", "leather", "microfiber", "sit", "relax"]
+ },
+ "couple": {
+ "unicode": "1F46B",
+ "unicode_alternates": [],
+ "name": "man and woman holding hands",
+ "shortname": ":couple:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "date", "dating", "human", "like", "love", "marriage", "people", "valentines"],
+ "moji": "👫"
+ },
+ "couple_mm": {
+ "unicode": "1F468-2764-1F468",
+ "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F468"],
+ "name": "couple (man,man)",
+ "shortname": ":couple_mm:",
+ "category": "people",
+ "aliases": [":couple_with_heart_mm:"],
+ "aliases_ascii": [],
+ "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"]
+ },
+ "couple_with_heart": {
+ "unicode": "1F491",
+ "unicode_alternates": [],
+ "name": "couple with heart",
+ "shortname": ":couple_with_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"],
+ "moji": "💑"
+ },
+ "couple_ww": {
+ "unicode": "1F469-2764-1F469",
+ "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F469"],
+ "name": "couple (woman,woman)",
+ "shortname": ":couple_ww:",
+ "category": "people",
+ "aliases": [":couple_with_heart_ww:"],
+ "aliases_ascii": [],
+ "keywords": ["affection", "dating", "human", "like", "love", "marriage", "valentines"]
+ },
+ "couplekiss": {
+ "unicode": "1F48F",
+ "unicode_alternates": [],
+ "name": "kiss",
+ "shortname": ":couplekiss:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dating", "like", "love", "marriage", "valentines"],
+ "moji": "💏"
+ },
+ "cow": {
+ "unicode": "1F42E",
+ "unicode_alternates": [],
+ "name": "cow face",
+ "shortname": ":cow:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "beef", "ox"],
+ "moji": "🐮"
+ },
+ "cow2": {
+ "unicode": "1F404",
+ "unicode_alternates": [],
+ "name": "cow",
+ "shortname": ":cow2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "beef", "nature", "ox", "cow", "milk", "dairy", "beef", "bessie", "moo"],
+ "moji": "🐄"
+ },
+ "crayon": {
+ "unicode": "1F58D",
+ "unicode_alternates": [],
+ "name": "lower left crayon",
+ "shortname": ":crayon:",
+ "category": "objects_symbols",
+ "aliases": [":lower_left_crayon:"],
+ "aliases_ascii": [],
+ "keywords": ["write", "draw", "color", "wax"]
+ },
+ "credit_card": {
+ "unicode": "1F4B3",
+ "unicode_alternates": [],
+ "name": "credit card",
+ "shortname": ":credit_card:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bill", "dollar", "money", "pay", "payment", "credit", "card", "loan", "purchase", "shopping", "mastercard", "visa", "american express", "wallet", "signature"],
+ "moji": "💳"
+ },
+ "crescent_moon": {
+ "unicode": "1F319",
+ "unicode_alternates": [],
+ "name": "crescent moon",
+ "shortname": ":crescent_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "moon", "crescent", "waxing", "sky", "night", "cheese", "phase"],
+ "moji": "🌙"
+ },
+ "crocodile": {
+ "unicode": "1F40A",
+ "unicode_alternates": [],
+ "name": "crocodile",
+ "shortname": ":crocodile:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "crocodile", "croc", "alligator", "gator", "cranky"],
+ "moji": "🐊"
+ },
+ "cross_heavy": {
+ "unicode": "1F547",
+ "unicode_alternates": [],
+ "name": "heavy latin cross",
+ "shortname": ":cross_heavy:",
+ "category": "objects_symbols",
+ "aliases": [":heavy_latin_cross:"],
+ "aliases_ascii": [],
+ "keywords": ["religion", "symbol"]
+ },
+ "cross_white": {
+ "unicode": "1F546",
+ "unicode_alternates": [],
+ "name": "white latin cross",
+ "shortname": ":cross_white:",
+ "category": "objects_symbols",
+ "aliases": [":white_latin_cross:"],
+ "aliases_ascii": [],
+ "keywords": ["religion", "symbol"]
+ },
+ "crossbones": {
+ "unicode": "1F571",
+ "unicode_alternates": [],
+ "name": "black skull and crossbones",
+ "shortname": ":crossbones:",
+ "category": "objects_symbols",
+ "aliases": [":black_skull_and_crossbones:"],
+ "aliases_ascii": [],
+ "keywords": ["poison", "danger", "death"]
+ },
+ "crossed_flags": {
+ "unicode": "1F38C",
+ "unicode_alternates": [],
+ "name": "crossed flags",
+ "shortname": ":crossed_flags:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["japan"],
+ "moji": "🎌"
+ },
+ "crown": {
+ "unicode": "1F451",
+ "unicode_alternates": [],
+ "name": "crown",
+ "shortname": ":crown:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["king", "kod", "leader", "royalty"],
+ "moji": "👑"
+ },
+ "cruise_ship": {
+ "unicode": "1F6F3",
+ "unicode_alternates": [],
+ "name": "passenger ship",
+ "shortname": ":cruise_ship:",
+ "category": "travel_places",
+ "aliases": [":passenger_ship:"],
+ "aliases_ascii": [],
+ "keywords": ["titanic", "transportation", "boat"]
+ },
+ "cry": {
+ "unicode": "1F622",
+ "unicode_alternates": [],
+ "name": "crying face",
+ "shortname": ":cry:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":'(", ":'-(", ";(", ";-("],
+ "keywords": ["face", "sad", "sad", "cry", "tear", "weep", "tears"],
+ "moji": "😢"
+ },
+ "crying_cat_face": {
+ "unicode": "1F63F",
+ "unicode_alternates": [],
+ "name": "crying cat face",
+ "shortname": ":crying_cat_face:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "sad", "tears", "weep", "cry", "cat", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"],
+ "moji": "😿"
+ },
+ "crystal_ball": {
+ "unicode": "1F52E",
+ "unicode_alternates": [],
+ "name": "crystal ball",
+ "shortname": ":crystal_ball:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["disco", "party"],
+ "moji": "🔮"
+ },
+ "cupid": {
+ "unicode": "1F498",
+ "unicode_alternates": [],
+ "name": "heart with arrow",
+ "shortname": ":cupid:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "heart", "like", "love", "valentines"],
+ "moji": "💘"
+ },
+ "curly_loop": {
+ "unicode": "27B0",
+ "unicode_alternates": [],
+ "name": "curly loop",
+ "shortname": ":curly_loop:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["scribble"],
+ "moji": "➰"
+ },
+ "currency_exchange": {
+ "unicode": "1F4B1",
+ "unicode_alternates": [],
+ "name": "currency exchange",
+ "shortname": ":currency_exchange:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dollar", "money", "travel"],
+ "moji": "💱"
+ },
+ "curry": {
+ "unicode": "1F35B",
+ "unicode_alternates": [],
+ "name": "curry and rice",
+ "shortname": ":curry:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "hot", "indian", "spicy", "curry", "spice", "flavor", "food", "meal"],
+ "moji": "🍛"
+ },
+ "custard": {
+ "unicode": "1F36E",
+ "unicode_alternates": [],
+ "name": "custard",
+ "shortname": ":custard:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "food", "custard", "cream", "rich", "butter", "dessert", "crème", "brûlée", "french"],
+ "moji": "🍮"
+ },
+ "customs": {
+ "unicode": "1F6C3",
+ "unicode_alternates": [],
+ "name": "customs",
+ "shortname": ":customs:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["border", "passport", "customs", "travel", "foreign", "goods", "check", "authority", "government"],
+ "moji": "🛃"
+ },
+ "cyclone": {
+ "moji": "🌀",
+ "unicode": "1F300",
+ "unicode_alternates": [],
+ "name": "cyclone",
+ "shortname": ":cyclone:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue", "cloud", "swirl", "weather", "cyclone", "hurricane", "typhoon", "storm", "ocean"]
+ },
+ "dagger": {
+ "unicode": "1F5E1",
+ "unicode_alternates": [],
+ "name": "dagger knife",
+ "shortname": ":dagger:",
+ "category": "objects_symbols",
+ "aliases": [":dagger_knife:"],
+ "aliases_ascii": [],
+ "keywords": ["blade", "knife"]
+ },
+ "dancer": {
+ "unicode": "1F483",
+ "unicode_alternates": [],
+ "name": "dancer",
+ "shortname": ":dancer:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "fun", "girl", "woman", "dance", "dancer", "dress", "fancy", "boogy", "party", "celebrate", "ballet", "tango", "cha cha", "music"],
+ "moji": "💃"
+ },
+ "dancers": {
+ "unicode": "1F46F",
+ "unicode_alternates": [],
+ "name": "woman with bunny ears",
+ "shortname": ":dancers:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bunny", "female", "girls", "women", "dancing", "dancers", "showgirl", "playboy", "costume", "bunny", "cancan"],
+ "moji": "👯"
+ },
+ "dango": {
+ "unicode": "1F361",
+ "unicode_alternates": [],
+ "name": "dango",
+ "shortname": ":dango:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "dango", "japanese", "dumpling", "mochi", "balls", "skewer"],
+ "moji": "🍡"
+ },
+ "dark_sunglasses": {
+ "unicode": "1F576",
+ "unicode_alternates": [],
+ "name": "dark sunglasses",
+ "shortname": ":dark_sunglasses:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shades", "eyes"]
+ },
+ "dart": {
+ "unicode": "1F3AF",
+ "unicode_alternates": [],
+ "name": "direct hit",
+ "shortname": ":dart:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bar", "game", "direct", "hit", "bullseye", "dart", "archery", "game", "fletching", "arrow", "sport"],
+ "moji": "🎯"
+ },
+ "dash": {
+ "unicode": "1F4A8",
+ "unicode_alternates": [],
+ "name": "dash symbol",
+ "shortname": ":dash:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["air", "fast", "shoo", "wind"],
+ "moji": "💨"
+ },
+ "date": {
+ "unicode": "1F4C5",
+ "unicode_alternates": [],
+ "name": "calendar",
+ "shortname": ":date:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["calendar", "schedule"],
+ "moji": "📅"
+ },
+ "deciduous_tree": {
+ "unicode": "1F333",
+ "unicode_alternates": [],
+ "name": "deciduous tree",
+ "shortname": ":deciduous_tree:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "deciduous", "tree", "leaves", "fall", "color"],
+ "moji": "🌳"
+ },
+ "department_store": {
+ "unicode": "1F3EC",
+ "unicode_alternates": [],
+ "name": "department store",
+ "shortname": ":department_store:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "mall", "shopping", "department", "store", "retail", "sale", "merchandise"],
+ "moji": "🏬"
+ },
+ "descending_notes": {
+ "unicode": "1F39D",
+ "unicode_alternates": [],
+ "name": "beamed descending musical notes",
+ "shortname": ":descending_notes:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["score", "music", "sound", "tone"]
+ },
+ "desert": {
+ "unicode": "1F3DC",
+ "unicode_alternates": [],
+ "name": "desert",
+ "shortname": ":desert:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["hot", "dry", "sandy", "cactus", "sunny", "barren"]
+ },
+ "desktop": {
+ "unicode": "1F5A5",
+ "unicode_alternates": [],
+ "name": "desktop computer",
+ "shortname": ":desktop:",
+ "category": "objects_symbols",
+ "aliases": [":desktop_computer:"],
+ "aliases_ascii": [],
+ "keywords": ["cpu"]
+ },
+ "desktop_window": {
+ "unicode": "1F5D4",
+ "unicode_alternates": [],
+ "name": "desktop window",
+ "shortname": ":desktop_window:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["computer"]
+ },
+ "diamond_shape_with_a_dot_inside": {
+ "unicode": "1F4A0",
+ "unicode_alternates": [],
+ "name": "diamond shape with a dot inside",
+ "shortname": ":diamond_shape_with_a_dot_inside:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["diamond", "cute", "cuteness", "kawaii", "japanese", "glyph", "adorable"],
+ "moji": "💠"
+ },
+ "diamonds": {
+ "unicode": "2666",
+ "unicode_alternates": ["2666-FE0F"],
+ "name": "black diamond suit",
+ "shortname": ":diamonds:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cards", "poker"],
+ "moji": "♦"
+ },
+ "disappointed": {
+ "unicode": "1F61E",
+ "unicode_alternates": [],
+ "name": "disappointed face",
+ "shortname": ":disappointed:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [">:[", ":-(", ":(", ":-[", ":[", "=("],
+ "keywords": ["disappointed", "disappoint", "frown", "depressed", "discouraged", "face", "sad", "upset"],
+ "moji": "😞"
+ },
+ "disappointed_relieved": {
+ "unicode": "1F625",
+ "unicode_alternates": [],
+ "name": "disappointed but relieved face",
+ "shortname": ":disappointed_relieved:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "nervous", "phew", "sweat", "disappoint", "relief"],
+ "moji": "😥"
+ },
+ "dividers": {
+ "unicode": "1F5C2",
+ "unicode_alternates": [],
+ "name": "card index dividers",
+ "shortname": ":dividers:",
+ "category": "objects_symbols",
+ "aliases": [":card_index_dividers:"],
+ "aliases_ascii": [],
+ "keywords": ["stationery", "rolodex"]
+ },
+ "dizzy": {
+ "unicode": "1F4AB",
+ "unicode_alternates": [],
+ "name": "dizzy symbol",
+ "shortname": ":dizzy:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shoot", "sparkle", "star", "dizzy", "drunk", "sick", "intoxicated", "squeans", "starburst", "star"],
+ "moji": "💫"
+ },
+ "dizzy_face": {
+ "unicode": "1F635",
+ "unicode_alternates": [],
+ "name": "dizzy face",
+ "shortname": ":dizzy_face:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["#-)", "#)", "%-)", "%)", "X)", "X-)"],
+ "keywords": ["dizzy", "drunk", "inebriated", "face", "spent", "unconscious", "xox"],
+ "moji": "😵"
+ },
+ "do_not_litter": {
+ "unicode": "1F6AF",
+ "unicode_alternates": [],
+ "name": "do not litter symbol",
+ "shortname": ":do_not_litter:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bin", "garbage", "trash", "litter", "garbage", "waste", "no", "can", "trash"],
+ "moji": "🚯"
+ },
+ "document": {
+ "unicode": "1F5CE",
+ "unicode_alternates": [],
+ "name": "document",
+ "shortname": ":document:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["page"]
+ },
+ "document_text": {
+ "unicode": "1F5B9",
+ "unicode_alternates": [],
+ "name": "document with text",
+ "shortname": ":document_text:",
+ "category": "objects_symbols",
+ "aliases": [":document_with_text:"],
+ "aliases_ascii": [],
+ "keywords": ["page"]
+ },
+ "dog": {
+ "unicode": "1F436",
+ "unicode_alternates": [],
+ "name": "dog face",
+ "shortname": ":dog:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "friend", "nature", "woof"],
+ "moji": "🐶"
+ },
+ "dog2": {
+ "unicode": "1F415",
+ "unicode_alternates": [],
+ "name": "dog",
+ "shortname": ":dog2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "doge", "friend", "nature", "pet", "dog", "puppy", "pet", "friend", "woof", "bark", "fido"],
+ "moji": "🐕"
+ },
+ "dollar": {
+ "unicode": "1F4B5",
+ "unicode_alternates": [],
+ "name": "banknote with dollar sign",
+ "shortname": ":dollar:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bill", "currency", "money", "dollar", "united states", "canada", "australia", "banknote", "money", "currency", "paper", "cash", "bills"],
+ "moji": "💵"
+ },
+ "dolls": {
+ "unicode": "1F38E",
+ "unicode_alternates": [],
+ "name": "japanese dolls",
+ "shortname": ":dolls:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["japanese", "kimono", "toy", "dolls", "japan", "japanese", "day", "girls", "emperor", "empress", "pray", "blessing", "imperial", "family", "royal"],
+ "moji": "🎎"
+ },
+ "dolphin": {
+ "unicode": "1F42C",
+ "unicode_alternates": [],
+ "name": "dolphin",
+ "shortname": ":dolphin:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "fins", "fish", "flipper", "nature", "ocean", "sea"],
+ "moji": "🐬"
+ },
+ "door": {
+ "unicode": "1F6AA",
+ "unicode_alternates": [],
+ "name": "door",
+ "shortname": ":door:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["entry", "exit", "house", "door", "doorway", "entrance", "enter", "exit", "entry"],
+ "moji": "🚪"
+ },
+ "doughnut": {
+ "unicode": "1F369",
+ "unicode_alternates": [],
+ "name": "doughnut",
+ "shortname": ":doughnut:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "food", "snack", "sweet", "doughnut", "donut", "pastry", "fried", "dessert", "breakfast", "police", "homer", "sweet"],
+ "moji": "🍩"
+ },
+ "dove": {
+ "unicode": "1F54A",
+ "unicode_alternates": [],
+ "name": "dove of peace",
+ "shortname": ":dove:",
+ "category": "objects_symbols",
+ "aliases": [":dove_of_peace:"],
+ "aliases_ascii": [],
+ "keywords": ["symbol", "bird"]
+ },
+ "dragon": {
+ "unicode": "1F409",
+ "unicode_alternates": [],
+ "name": "dragon",
+ "shortname": ":dragon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "fire", "legendary", "myth"],
+ "moji": "🐉"
+ },
+ "dragon_face": {
+ "unicode": "1F432",
+ "unicode_alternates": [],
+ "name": "dragon face",
+ "shortname": ":dragon_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "chinese", "green", "myth", "nature", "dragon", "head", "fire", "legendary", "myth"],
+ "moji": "🐲"
+ },
+ "dress": {
+ "unicode": "1F457",
+ "unicode_alternates": [],
+ "name": "dress",
+ "shortname": ":dress:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clothes", "fashion"],
+ "moji": "👗"
+ },
+ "dromedary_camel": {
+ "unicode": "1F42A",
+ "unicode_alternates": [],
+ "name": "dromedary camel",
+ "shortname": ":dromedary_camel:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "desert", "hot", "dromedary", "camel", "hump", "desert", "middle east", "heat", "hot", "water", "hump day", "wednesday", "sex"],
+ "moji": "🐪"
+ },
+ "droplet": {
+ "unicode": "1F4A7",
+ "unicode_alternates": [],
+ "name": "droplet",
+ "shortname": ":droplet:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["drip", "faucet", "water", "drop", "droplet", "h20", "water", "aqua", "tear", "sweat", "rain", "moisture", "wet", "moist", "spit"],
+ "moji": "💧"
+ },
+ "dvd": {
+ "unicode": "1F4C0",
+ "unicode_alternates": [],
+ "name": "dvd",
+ "shortname": ":dvd:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cd", "disc", "disk"],
+ "moji": "📀"
+ },
+ "e-mail": {
+ "unicode": "1F4E7",
+ "unicode_alternates": [],
+ "name": "e-mail symbol",
+ "shortname": ":e-mail:",
+ "category": "objects",
+ "aliases": [":email:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "inbox"],
+ "moji": "📧"
+ },
+ "ear": {
+ "unicode": "1F442",
+ "unicode_alternates": [],
+ "name": "ear",
+ "shortname": ":ear:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "hear", "listen", "sound"],
+ "moji": "👂"
+ },
+ "ear_of_rice": {
+ "unicode": "1F33E",
+ "unicode_alternates": [],
+ "name": "ear of rice",
+ "shortname": ":ear_of_rice:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "ear", "rice", "food", "plant", "seed"],
+ "moji": "🌾"
+ },
+ "earth_africa": {
+ "unicode": "1F30D",
+ "unicode_alternates": [],
+ "name": "earth globe europe-africa",
+ "shortname": ":earth_africa:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["globe", "international", "world", "earth", "globe", "space", "planet", "africa", "europe", "home"],
+ "moji": "🌍"
+ },
+ "earth_americas": {
+ "unicode": "1F30E",
+ "unicode_alternates": [],
+ "name": "earth globe americas",
+ "shortname": ":earth_americas:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["USA", "globe", "international", "world", "earth", "globe", "space", "planet", "north", "south", "america", "americas", "home"],
+ "moji": "🌎"
+ },
+ "earth_asia": {
+ "unicode": "1F30F",
+ "unicode_alternates": [],
+ "name": "earth globe asia-australia",
+ "shortname": ":earth_asia:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["east", "globe", "international", "world", "earth", "globe", "space", "planet", "asia", "australia", "home"],
+ "moji": "🌏"
+ },
+ "egg": {
+ "unicode": "1F373",
+ "unicode_alternates": [],
+ "name": "cooking",
+ "shortname": ":egg:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["breakfast", "food", "egg", "fry", "pan", "flat", "cook", "frying", "cooking", "utensil"],
+ "moji": "🍳"
+ },
+ "eggplant": {
+ "unicode": "1F346",
+ "unicode_alternates": [],
+ "name": "aubergine",
+ "shortname": ":eggplant:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["aubergine", "food", "nature", "vegetable", "eggplant", "aubergine", "fruit", "purple", "penis"],
+ "moji": "🍆"
+ },
+ "eight": {
+ "moji": "8️⃣",
+ "unicode": "0038-20E3",
+ "unicode_alternates": ["0038-FE0F-20E3"],
+ "name": "digit eight",
+ "shortname": ":eight:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["8", "blue-square", "numbers"]
+ },
+ "eight_pointed_black_star": {
+ "unicode": "2734",
+ "unicode_alternates": ["2734-FE0F"],
+ "name": "eight pointed black star",
+ "shortname": ":eight_pointed_black_star:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "✴"
+ },
+ "eight_spoked_asterisk": {
+ "unicode": "2733",
+ "unicode_alternates": ["2733-FE0F"],
+ "name": "eight spoked asterisk",
+ "shortname": ":eight_spoked_asterisk:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["green-square", "sparkle", "star"],
+ "moji": "✳"
+ },
+ "electric_plug": {
+ "unicode": "1F50C",
+ "unicode_alternates": [],
+ "name": "electric plug",
+ "shortname": ":electric_plug:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["charger", "power"],
+ "moji": "🔌"
+ },
+ "elephant": {
+ "unicode": "1F418",
+ "unicode_alternates": [],
+ "name": "elephant",
+ "shortname": ":elephant:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "nose", "thailand"],
+ "moji": "🐘"
+ },
+ "end": {
+ "unicode": "1F51A",
+ "unicode_alternates": [],
+ "name": "end with leftwards arrow above",
+ "shortname": ":end:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "words"],
+ "moji": "🔚"
+ },
+ "envelope": {
+ "unicode": "2709",
+ "unicode_alternates": ["2709-FE0F"],
+ "name": "envelope",
+ "shortname": ":envelope:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "letter", "mail", "postal"],
+ "moji": "✉"
+ },
+ "envelope_back": {
+ "unicode": "1F582",
+ "unicode_alternates": [],
+ "name": "back of envelope",
+ "shortname": ":envelope_back:",
+ "category": "objects_symbols",
+ "aliases": [":back_of_envelope:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "letter", "mail", "postal"]
+ },
+ "envelope_flying": {
+ "unicode": "1F585",
+ "unicode_alternates": [],
+ "name": "flying envelope",
+ "shortname": ":envelope_flying:",
+ "category": "objects_symbols",
+ "aliases": [":flying_envelope:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "letter", "mail", "postal"]
+ },
+ "envelope_stamped": {
+ "unicode": "1F583",
+ "unicode_alternates": [],
+ "name": "stamped envelope",
+ "shortname": ":envelope_stamped:",
+ "category": "objects_symbols",
+ "aliases": [":stamped_envelope:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "letter", "mail", "postal"]
+ },
+ "envelope_stamped_pen": {
+ "unicode": "1F586",
+ "unicode_alternates": [],
+ "name": "pen over stamped envelope",
+ "shortname": ":envelope_stamped_pen:",
+ "category": "objects_symbols",
+ "aliases": [":pen_over_stamped_envelope:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "letter", "mail", "postal"]
+ },
+ "envelope_with_arrow": {
+ "unicode": "1F4E9",
+ "unicode_alternates": [],
+ "name": "envelope with downwards arrow above",
+ "shortname": ":envelope_with_arrow:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["email"],
+ "moji": "📩"
+ },
+ "euro": {
+ "unicode": "1F4B6",
+ "unicode_alternates": [],
+ "name": "banknote with euro sign",
+ "shortname": ":euro:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["currency", "dollar", "money", "euro", "europe", "banknote", "money", "currency", "paper", "cash", "bills"],
+ "moji": "💶"
+ },
+ "european_castle": {
+ "unicode": "1F3F0",
+ "unicode_alternates": [],
+ "name": "european castle",
+ "shortname": ":european_castle:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "history", "royalty", "castle", "european", "residence", "royalty", "disneyland", "disney", "fort", "fortified", "moat", "tower", "princess", "prince", "lord", "king", "queen", "fortress", "nobel", "stronghold"],
+ "moji": "🏰"
+ },
+ "european_post_office": {
+ "unicode": "1F3E4",
+ "unicode_alternates": [],
+ "name": "european post office",
+ "shortname": ":european_post_office:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building"],
+ "moji": "🏤"
+ },
+ "evergreen_tree": {
+ "unicode": "1F332",
+ "unicode_alternates": [],
+ "name": "evergreen tree",
+ "shortname": ":evergreen_tree:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "evergreen", "tree", "needles", "christmas"],
+ "moji": "🌲"
+ },
+ "exclamation": {
+ "unicode": "2757",
+ "unicode_alternates": ["2757-FE0F"],
+ "name": "heavy exclamation mark symbol",
+ "shortname": ":exclamation:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["surprise"],
+ "moji": "❗"
+ },
+ "expressionless": {
+ "unicode": "1F611",
+ "unicode_alternates": [],
+ "name": "expressionless face",
+ "shortname": ":expressionless:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["-_-", "-__-", "-___-"],
+ "keywords": ["expressionless", "blank", "void", "vapid", "without expression", "face", "indifferent"],
+ "moji": "😑"
+ },
+ "eye": {
+ "unicode": "1F441",
+ "unicode_alternates": [],
+ "name": "eye",
+ "shortname": ":eye:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["look", "peek", "watch"]
+ },
+ "eyeglasses": {
+ "unicode": "1F453",
+ "unicode_alternates": [],
+ "name": "eyeglasses",
+ "shortname": ":eyeglasses:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accessories", "eyesight", "fashion", "eyeglasses", "spectacles", "eye", "sight", "nearsightedness", "myopia", "farsightedness", "hyperopia", "frames", "vision", "see", "blurry", "contacts"],
+ "moji": "👓"
+ },
+ "eyes": {
+ "unicode": "1F440",
+ "unicode_alternates": [],
+ "name": "eyes",
+ "shortname": ":eyes:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["look", "peek", "stalk", "watch"],
+ "moji": "👀"
+ },
+ "factory": {
+ "unicode": "1F3ED",
+ "unicode_alternates": [],
+ "name": "factory",
+ "shortname": ":factory:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building"],
+ "moji": "🏭"
+ },
+ "fallen_leaf": {
+ "unicode": "1F342",
+ "unicode_alternates": [],
+ "name": "fallen leaf",
+ "shortname": ":fallen_leaf:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["leaves", "nature", "plant", "vegetable", "leaf", "fall", "color", "deciduous", "autumn"],
+ "moji": "🍂"
+ },
+ "family": {
+ "unicode": "1F46A",
+ "unicode_alternates": [],
+ "name": "family",
+ "shortname": ":family:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["child", "dad", "father", "home", "mom", "mother", "parents", "family", "mother", "father", "child", "girl", "boy", "group", "unit"],
+ "moji": "👪"
+ },
+ "family_mmb": {
+ "unicode": "1F468-1F468-1F466",
+ "unicode_alternates": ["1F468-200D-1F468-200D-1F466"],
+ "name": "family (man,man,boy)",
+ "shortname": ":family_mmb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"]
+ },
+ "family_mmbb": {
+ "unicode": "1F468-1F468-1F466-1F466",
+ "unicode_alternates": ["1F468-200D-1F468-200D-1F466-200D-1F466"],
+ "name": "family (man,man,boy,boy)",
+ "shortname": ":family_mmbb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "boy"]
+ },
+ "family_mmg": {
+ "unicode": "1F468-1F468-1F467",
+ "unicode_alternates": ["1F468-200D-1F468-200D-1F467"],
+ "name": "family (man,man,girl)",
+ "shortname": ":family_mmg:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["child", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"]
+ },
+ "family_mmgb": {
+ "unicode": "1F468-1F468-1F467-1F466",
+ "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F466"],
+ "name": "family (man,man,girl,boy)",
+ "shortname": ":family_mmgb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl", "boy"]
+ },
+ "family_mmgg": {
+ "unicode": "1F468-1F468-1F467-1F467",
+ "unicode_alternates": ["1F468-200D-1F468-200D-1F467-200D-1F467"],
+ "name": "family (man,man,girl,girl)",
+ "shortname": ":family_mmgg:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["children", "dad", "father", "parents", "group", "unit", "gay", "homosexual", "man", "girl"]
+ },
+ "family_mwbb": {
+ "unicode": "1F468-1F469-1F466-1F466",
+ "unicode_alternates": ["1F468-200D-1F469-200D-1F466-200D-1F466"],
+ "name": "family (man,woman,boy,boy)",
+ "shortname": ":family_mwbb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dad", "father", "mom", "mother", "parents", "children", "boy", "group", "unit", "man", "woman"]
+ },
+ "family_mwg": {
+ "unicode": "1F468-1F469-1F467",
+ "unicode_alternates": ["1F468-200D-1F469-200D-1F467"],
+ "name": "family (man,woman,girl)",
+ "shortname": ":family_mwg:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["child", "dad", "father", "mom", "mother", "parents", "girl", "boy", "group", "unit", "man", "woman"]
+ },
+ "family_mwgb": {
+ "unicode": "1F468-1F469-1F467-1F466",
+ "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F466"],
+ "name": "family (man,woman,girl,boy)",
+ "shortname": ":family_mwgb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "boy", "group", "unit", "man", "woman"]
+ },
+ "family_mwgg": {
+ "unicode": "1F468-1F469-1F467-1F467",
+ "unicode_alternates": ["1F468-200D-1F469-200D-1F467-200D-1F467"],
+ "name": "family (man,woman,girl,girl)",
+ "shortname": ":family_mwgg:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dad", "father", "mom", "mother", "parents", "children", "girl", "group", "unit", "man", "woman"]
+ },
+ "family_wwb": {
+ "unicode": "1F469-1F469-1F466",
+ "unicode_alternates": ["1F469-200D-1F469-200D-1F466"],
+ "name": "family (woman,woman,boy)",
+ "shortname": ":family_wwb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mom", "mother", "parents", "child", "boy", "group", "unit", "gay", "lesbian", "homosexual", "woman"]
+ },
+ "family_wwbb": {
+ "unicode": "1F469-1F469-1F466-1F466",
+ "unicode_alternates": ["1F469-200D-1F469-200D-1F466-200D-1F466"],
+ "name": "family (woman,woman,boy,boy)",
+ "shortname": ":family_wwbb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "boy"]
+ },
+ "family_wwg": {
+ "unicode": "1F469-1F469-1F467",
+ "unicode_alternates": ["1F469-200D-1F469-200D-1F467"],
+ "name": "family (woman,woman,girl)",
+ "shortname": ":family_wwg:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mom", "mother", "parents", "child", "woman", "girl", "group", "unit", "gay", "lesbian", "homosexual"]
+ },
+ "family_wwgb": {
+ "unicode": "1F469-1F469-1F467-1F466",
+ "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F466"],
+ "name": "family (woman,woman,girl,boy)",
+ "shortname": ":family_wwgb:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl", "boy"]
+ },
+ "family_wwgg": {
+ "unicode": "1F469-1F469-1F467-1F467",
+ "unicode_alternates": ["1F469-200D-1F469-200D-1F467-200D-1F467"],
+ "name": "family (woman,woman,girl,girl)",
+ "shortname": ":family_wwgg:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mom", "mother", "parents", "children", "group", "unit", "gay", "lesbian", "homosexual", "woman", "girl"]
+ },
+ "fast_forward": {
+ "unicode": "23E9",
+ "unicode_alternates": [],
+ "name": "black right-pointing double triangle",
+ "shortname": ":fast_forward:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "⏩"
+ },
+ "fax": {
+ "unicode": "1F4E0",
+ "unicode_alternates": [],
+ "name": "fax machine",
+ "shortname": ":fax:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "technology"],
+ "moji": "📠"
+ },
+ "fearful": {
+ "unicode": "1F628",
+ "unicode_alternates": [],
+ "name": "fearful face",
+ "shortname": ":fearful:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "nervous", "oops", "scared", "terrified", "fear", "fearful", "scared", "frightened"],
+ "moji": "😨"
+ },
+ "feet": {
+ "unicode": "1F43E",
+ "unicode_alternates": [],
+ "name": "paw prints",
+ "shortname": ":feet:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cat", "dog", "footprints", "paw", "pet", "tracking", "paw", "prints", "mark", "imprints", "footsteps", "animal", "lion", "bear", "dog", "cat", "raccoon", "critter", "feet", "pawsteps"],
+ "moji": "🐾"
+ },
+ "ferris_wheel": {
+ "unicode": "1F3A1",
+ "unicode_alternates": [],
+ "name": "ferris wheel",
+ "shortname": ":ferris_wheel:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["carnival", "londoneye", "photo", "farris", "wheel", "amusement", "park", "fair", "ride", "entertainment"],
+ "moji": "🎡"
+ },
+ "file_cabinet": {
+ "unicode": "1F5C4",
+ "unicode_alternates": [],
+ "name": "file cabinet",
+ "shortname": ":file_cabinet:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["folders", "office", "documents", "storage"]
+ },
+ "file_folder": {
+ "unicode": "1F4C1",
+ "unicode_alternates": [],
+ "name": "file folder",
+ "shortname": ":file_folder:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents"],
+ "moji": "📁"
+ },
+ "film_frames": {
+ "unicode": "1F39E",
+ "unicode_alternates": [],
+ "name": "film frames",
+ "shortname": ":film_frames:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["movie", "record", "8mm", "16mm", "reel", "celluloid"]
+ },
+ "finger_pointing_down": {
+ "unicode": "1F597",
+ "unicode_alternates": [],
+ "name": "white down pointing left hand index",
+ "shortname": ":finger_pointing_down:",
+ "category": "people",
+ "aliases": [":white_down_pointing_left_hand_index:"],
+ "aliases_ascii": [],
+ "keywords": ["direction", "finger", "hand"]
+ },
+ "finger_pointing_down2": {
+ "unicode": "1F59F",
+ "unicode_alternates": [],
+ "name": "sideways white down pointing index",
+ "shortname": ":finger_pointing_down2:",
+ "category": "people",
+ "aliases": [":sideways_white_down_pointing_index:"],
+ "aliases_ascii": [],
+ "keywords": ["direction", "finger", "hand"]
+ },
+ "finger_pointing_left": {
+ "unicode": "1F598",
+ "unicode_alternates": [],
+ "name": "sideways white left pointing index",
+ "shortname": ":finger_pointing_left:",
+ "category": "people",
+ "aliases": [":sideways_white_left_pointing_index:"],
+ "aliases_ascii": [],
+ "keywords": ["direction", "finger", "hand"]
+ },
+ "finger_pointing_right": {
+ "unicode": "1F599",
+ "unicode_alternates": [],
+ "name": "sideways white right pointing index",
+ "shortname": ":finger_pointing_right:",
+ "category": "people",
+ "aliases": [":sideways_white_right_pointing_index:"],
+ "aliases_ascii": [],
+ "keywords": ["direction", "finger", "hand"]
+ },
+ "finger_pointing_up": {
+ "unicode": "1F59E",
+ "unicode_alternates": [],
+ "name": "sideways white up pointing index",
+ "shortname": ":finger_pointing_up:",
+ "category": "people",
+ "aliases": [":sideways_white_up_pointing_index:"],
+ "aliases_ascii": [],
+ "keywords": ["direction", "finger", "hand"]
+ },
+ "fire": {
+ "unicode": "1F525",
+ "unicode_alternates": [],
+ "name": "fire",
+ "shortname": ":fire:",
+ "category": "emoticons",
+ "aliases": [":flame:"],
+ "aliases_ascii": [],
+ "keywords": ["cook", "hot", "flame"],
+ "moji": "🔥"
+ },
+ "fire_engine": {
+ "unicode": "1F692",
+ "unicode_alternates": [],
+ "name": "fire engine",
+ "shortname": ":fire_engine:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cars", "transportation", "vehicle", "fire", "fighter", "engine", "truck", "emergency", "medical"],
+ "moji": "🚒"
+ },
+ "fire_engine_oncoming": {
+ "unicode": "1F6F1",
+ "unicode_alternates": [],
+ "name": "oncoming fire engine",
+ "shortname": ":fire_engine_oncoming:",
+ "category": "travel_places",
+ "aliases": [":oncoming_fire_engine:"],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "fighter", "truck", "emergency"]
+ },
+ "fireworks": {
+ "unicode": "1F386",
+ "unicode_alternates": [],
+ "name": "fireworks",
+ "shortname": ":fireworks:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["carnival", "congratulations", "festival", "photo", "fireworks", "independence", "celebration", "explosion", "july", "4th", "rocket", "sky", "idea", "excitement"],
+ "moji": "🎆"
+ },
+ "first_quarter_moon": {
+ "unicode": "1F313",
+ "unicode_alternates": [],
+ "name": "first quarter moon symbol",
+ "shortname": ":first_quarter_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "quarter", "first", "sky", "night", "cheese", "phase"],
+ "moji": "🌓"
+ },
+ "first_quarter_moon_with_face": {
+ "unicode": "1F31B",
+ "unicode_alternates": [],
+ "name": "first quarter moon with face",
+ "shortname": ":first_quarter_moon_with_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "first", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"],
+ "moji": "🌛"
+ },
+ "fish": {
+ "unicode": "1F41F",
+ "unicode_alternates": [],
+ "name": "fish",
+ "shortname": ":fish:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "food", "nature"],
+ "moji": "🐟"
+ },
+ "fish_cake": {
+ "unicode": "1F365",
+ "unicode_alternates": [],
+ "name": "fish cake with swirl design",
+ "shortname": ":fish_cake:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fish", "cake", "kamboko", "swirl", "ramen", "noodles", "naruto"],
+ "moji": "🍥"
+ },
+ "fishing_pole_and_fish": {
+ "unicode": "1F3A3",
+ "unicode_alternates": [],
+ "name": "fishing pole and fish",
+ "shortname": ":fishing_pole_and_fish:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "hobby", "fish", "fishing", "pole"],
+ "moji": "🎣"
+ },
+ "fist": {
+ "unicode": "270A",
+ "unicode_alternates": [],
+ "name": "raised fist",
+ "shortname": ":fist:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fingers", "grasp", "hand"],
+ "moji": "✊"
+ },
+ "five": {
+ "moji": "5️⃣",
+ "unicode": "0035-20E3",
+ "unicode_alternates": ["0035-FE0F-20E3"],
+ "name": "digit five",
+ "shortname": ":five:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "numbers", "prime"]
+ },
+ "flag_ac": {
+ "unicode": "1F1E6-1F1E8",
+ "unicode_alternates": [],
+ "name": "ascension",
+ "shortname": ":flag_ac:",
+ "category": "flags",
+ "aliases": [":ac:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ac"]
+ },
+ "flag_ad": {
+ "unicode": "1F1E6-1F1E9",
+ "unicode_alternates": [],
+ "name": "andorra",
+ "shortname": ":flag_ad:",
+ "category": "flags",
+ "aliases": [":ad:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ad"]
+ },
+ "flag_ae": {
+ "unicode": "1F1E6-1F1EA",
+ "unicode_alternates": [],
+ "name": "the united arab emirates",
+ "shortname": ":flag_ae:",
+ "category": "flags",
+ "aliases": [":ae:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ae"]
+ },
+ "flag_af": {
+ "unicode": "1F1E6-1F1EB",
+ "unicode_alternates": [],
+ "name": "afghanistan",
+ "shortname": ":flag_af:",
+ "category": "flags",
+ "aliases": [":af:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "afghanestan", "af"]
+ },
+ "flag_ag": {
+ "unicode": "1F1E6-1F1EC",
+ "unicode_alternates": [],
+ "name": "antigua and barbuda",
+ "shortname": ":flag_ag:",
+ "category": "flags",
+ "aliases": [":ag:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ag"]
+ },
+ "flag_ai": {
+ "unicode": "1F1E6-1F1EE",
+ "unicode_alternates": [],
+ "name": "anguilla",
+ "shortname": ":flag_ai:",
+ "category": "flags",
+ "aliases": [":ai:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ai"]
+ },
+ "flag_al": {
+ "unicode": "1F1E6-1F1F1",
+ "unicode_alternates": [],
+ "name": "albania",
+ "shortname": ":flag_al:",
+ "category": "flags",
+ "aliases": [":al:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "shqiperia", "al"]
+ },
+ "flag_am": {
+ "unicode": "1F1E6-1F1F2",
+ "unicode_alternates": [],
+ "name": "armenia",
+ "shortname": ":flag_am:",
+ "category": "flags",
+ "aliases": [":am:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "hayastan", "am"]
+ },
+ "flag_ao": {
+ "unicode": "1F1E6-1F1F4",
+ "unicode_alternates": [],
+ "name": "angola",
+ "shortname": ":flag_ao:",
+ "category": "flags",
+ "aliases": [":ao:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ao"]
+ },
+ "flag_ar": {
+ "unicode": "1F1E6-1F1F7",
+ "unicode_alternates": [],
+ "name": "argentina",
+ "shortname": ":flag_ar:",
+ "category": "flags",
+ "aliases": [":ar:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ar"]
+ },
+ "flag_at": {
+ "unicode": "1F1E6-1F1F9",
+ "unicode_alternates": [],
+ "name": "austria",
+ "shortname": ":flag_at:",
+ "category": "flags",
+ "aliases": [":at:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "&ouml;sterreich", "osterreich", "at"]
+ },
+ "flag_au": {
+ "unicode": "1F1E6-1F1FA",
+ "unicode_alternates": [],
+ "name": "australia",
+ "shortname": ":flag_au:",
+ "category": "flags",
+ "aliases": [":au:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "au"]
+ },
+ "flag_aw": {
+ "unicode": "1F1E6-1F1FC",
+ "unicode_alternates": [],
+ "name": "aruba",
+ "shortname": ":flag_aw:",
+ "category": "flags",
+ "aliases": [":aw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "aw"]
+ },
+ "flag_az": {
+ "unicode": "1F1E6-1F1FF",
+ "unicode_alternates": [],
+ "name": "azerbaijan",
+ "shortname": ":flag_az:",
+ "category": "flags",
+ "aliases": [":az:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "azarbaycan", "az"]
+ },
+ "flag_ba": {
+ "unicode": "1F1E7-1F1E6",
+ "unicode_alternates": [],
+ "name": "bosnia and herzegovina",
+ "shortname": ":flag_ba:",
+ "category": "flags",
+ "aliases": [":ba:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bosna i hercegovina", "ba"]
+ },
+ "flag_bb": {
+ "unicode": "1F1E7-1F1E7",
+ "unicode_alternates": [],
+ "name": "barbados",
+ "shortname": ":flag_bb:",
+ "category": "flags",
+ "aliases": [":bb:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bb"]
+ },
+ "flag_bd": {
+ "unicode": "1F1E7-1F1E9",
+ "unicode_alternates": [],
+ "name": "bangladesh",
+ "shortname": ":flag_bd:",
+ "category": "flags",
+ "aliases": [":bd:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bd"]
+ },
+ "flag_be": {
+ "unicode": "1F1E7-1F1EA",
+ "unicode_alternates": [],
+ "name": "belgium",
+ "shortname": ":flag_be:",
+ "category": "flags",
+ "aliases": [":be:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "belgique", "belgie", "be"]
+ },
+ "flag_bf": {
+ "unicode": "1F1E7-1F1EB",
+ "unicode_alternates": [],
+ "name": "burkina faso",
+ "shortname": ":flag_bf:",
+ "category": "flags",
+ "aliases": [":bf:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bf"]
+ },
+ "flag_bg": {
+ "unicode": "1F1E7-1F1EC",
+ "unicode_alternates": [],
+ "name": "bulgaria",
+ "shortname": ":flag_bg:",
+ "category": "flags",
+ "aliases": [":bg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bg"]
+ },
+ "flag_bh": {
+ "unicode": "1F1E7-1F1ED",
+ "unicode_alternates": [],
+ "name": "bahrain",
+ "shortname": ":flag_bh:",
+ "category": "flags",
+ "aliases": [":bh:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al bahrayn", "bh"]
+ },
+ "flag_bi": {
+ "unicode": "1F1E7-1F1EE",
+ "unicode_alternates": [],
+ "name": "burundi",
+ "shortname": ":flag_bi:",
+ "category": "flags",
+ "aliases": [":bi:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bi"]
+ },
+ "flag_bj": {
+ "unicode": "1F1E7-1F1EF",
+ "unicode_alternates": [],
+ "name": "benin",
+ "shortname": ":flag_bj:",
+ "category": "flags",
+ "aliases": [":bj:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bj"]
+ },
+ "flag_black": {
+ "unicode": "1F3F4",
+ "unicode_alternates": [],
+ "name": "waving black flag",
+ "shortname": ":flag_black:",
+ "category": "objects_symbols",
+ "aliases": [":waving_black_flag:"],
+ "aliases_ascii": [],
+ "keywords": ["symbol", "signal"]
+ },
+ "flag_bm": {
+ "unicode": "1F1E7-1F1F2",
+ "unicode_alternates": [],
+ "name": "bermuda",
+ "shortname": ":flag_bm:",
+ "category": "flags",
+ "aliases": [":bm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bm"]
+ },
+ "flag_bn": {
+ "unicode": "1F1E7-1F1F3",
+ "unicode_alternates": [],
+ "name": "brunei",
+ "shortname": ":flag_bn:",
+ "category": "flags",
+ "aliases": [":bn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bn"]
+ },
+ "flag_bo": {
+ "unicode": "1F1E7-1F1F4",
+ "unicode_alternates": [],
+ "name": "bolivia",
+ "shortname": ":flag_bo:",
+ "category": "flags",
+ "aliases": [":bo:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bo"]
+ },
+ "flag_br": {
+ "unicode": "1F1E7-1F1F7",
+ "unicode_alternates": [],
+ "name": "brazil",
+ "shortname": ":flag_br:",
+ "category": "flags",
+ "aliases": [":br:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "brasil", "br"]
+ },
+ "flag_bs": {
+ "unicode": "1F1E7-1F1F8",
+ "unicode_alternates": [],
+ "name": "the bahamas",
+ "shortname": ":flag_bs:",
+ "category": "flags",
+ "aliases": [":bs:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bs"]
+ },
+ "flag_bt": {
+ "unicode": "1F1E7-1F1F9",
+ "unicode_alternates": [],
+ "name": "bhutan",
+ "shortname": ":flag_bt:",
+ "category": "flags",
+ "aliases": [":bt:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bt"]
+ },
+ "flag_bw": {
+ "unicode": "1F1E7-1F1FC",
+ "unicode_alternates": [],
+ "name": "botswana",
+ "shortname": ":flag_bw:",
+ "category": "flags",
+ "aliases": [":bw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bw"]
+ },
+ "flag_by": {
+ "unicode": "1F1E7-1F1FE",
+ "unicode_alternates": [],
+ "name": "belarus",
+ "shortname": ":flag_by:",
+ "category": "flags",
+ "aliases": [":by:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "byelarus", "by"]
+ },
+ "flag_bz": {
+ "unicode": "1F1E7-1F1FF",
+ "unicode_alternates": [],
+ "name": "belize",
+ "shortname": ":flag_bz:",
+ "category": "flags",
+ "aliases": [":bz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bz"]
+ },
+ "flag_ca": {
+ "unicode": "1F1E8-1F1E6",
+ "unicode_alternates": [],
+ "name": "canada",
+ "shortname": ":flag_ca:",
+ "category": "flags",
+ "aliases": [":ca:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ca"]
+ },
+ "flag_cd": {
+ "unicode": "1F1E8-1F1E9",
+ "unicode_alternates": [],
+ "name": "the democratic republic of the congo",
+ "shortname": ":flag_cd:",
+ "category": "flags",
+ "aliases": [":congo:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "r&eacute;publique d&eacute;mocratique du congo", "republique democratique du congo", "cd"]
+ },
+ "flag_cf": {
+ "unicode": "1F1E8-1F1EB",
+ "unicode_alternates": [],
+ "name": "central african republic",
+ "shortname": ":flag_cf:",
+ "category": "flags",
+ "aliases": [":cf:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cf"]
+ },
+ "flag_cg": {
+ "unicode": "1F1E8-1F1EC",
+ "unicode_alternates": [],
+ "name": "the republic of the congo",
+ "shortname": ":flag_cg:",
+ "category": "flags",
+ "aliases": [":cg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cg"]
+ },
+ "flag_ch": {
+ "unicode": "1F1E8-1F1ED",
+ "unicode_alternates": [],
+ "name": "switzerland",
+ "shortname": ":flag_ch:",
+ "category": "flags",
+ "aliases": [":ch:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "swiss"]
+ },
+ "flag_ci": {
+ "unicode": "1F1E8-1F1EE",
+ "unicode_alternates": [],
+ "name": "cote d'ivoire",
+ "shortname": ":flag_ci:",
+ "category": "flags",
+ "aliases": [":ci:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ci"]
+ },
+ "flag_cl": {
+ "unicode": "1F1E8-1F1F1",
+ "unicode_alternates": [],
+ "name": "chile",
+ "shortname": ":flag_cl:",
+ "category": "flags",
+ "aliases": [":chile:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cl"]
+ },
+ "flag_cm": {
+ "unicode": "1F1E8-1F1F2",
+ "unicode_alternates": [],
+ "name": "cameroon",
+ "shortname": ":flag_cm:",
+ "category": "flags",
+ "aliases": [":cm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cm"]
+ },
+ "flag_cn": {
+ "unicode": "1F1E8-1F1F3",
+ "unicode_alternates": [],
+ "name": "china",
+ "shortname": ":flag_cn:",
+ "category": "flags",
+ "aliases": [":cn:"],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "prc", "zhong guo", "country", "nation", "cn"]
+ },
+ "flag_co": {
+ "unicode": "1F1E8-1F1F4",
+ "unicode_alternates": [],
+ "name": "colombia",
+ "shortname": ":flag_co:",
+ "category": "flags",
+ "aliases": [":co:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "co"]
+ },
+ "flag_cr": {
+ "unicode": "1F1E8-1F1F7",
+ "unicode_alternates": [],
+ "name": "costa rica",
+ "shortname": ":flag_cr:",
+ "category": "flags",
+ "aliases": [":cr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cr"]
+ },
+ "flag_cu": {
+ "unicode": "1F1E8-1F1FA",
+ "unicode_alternates": [],
+ "name": "cuba",
+ "shortname": ":flag_cu:",
+ "category": "flags",
+ "aliases": [":cu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cu"]
+ },
+ "flag_cv": {
+ "unicode": "1F1E8-1F1FB",
+ "unicode_alternates": [],
+ "name": "cape verde",
+ "shortname": ":flag_cv:",
+ "category": "flags",
+ "aliases": [":cv:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "cabo verde", "cv"]
+ },
+ "flag_cy": {
+ "unicode": "1F1E8-1F1FE",
+ "unicode_alternates": [],
+ "name": "cyprus",
+ "shortname": ":flag_cy:",
+ "category": "flags",
+ "aliases": [":cy:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kibris", "kypros", "cy"]
+ },
+ "flag_cz": {
+ "unicode": "1F1E8-1F1FF",
+ "unicode_alternates": [],
+ "name": "the czech republic",
+ "shortname": ":flag_cz:",
+ "category": "flags",
+ "aliases": [":cz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ceska republika", "cz"]
+ },
+ "flag_de": {
+ "unicode": "1F1E9-1F1EA",
+ "unicode_alternates": [],
+ "name": "germany",
+ "shortname": ":flag_de:",
+ "category": "flags",
+ "aliases": [":de:"],
+ "aliases_ascii": [],
+ "keywords": ["german", "nation", "deutschland", "country", "de"]
+ },
+ "flag_dj": {
+ "unicode": "1F1E9-1F1EF",
+ "unicode_alternates": [],
+ "name": "djibouti",
+ "shortname": ":flag_dj:",
+ "category": "flags",
+ "aliases": [":dj:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "dj"]
+ },
+ "flag_dk": {
+ "unicode": "1F1E9-1F1F0",
+ "unicode_alternates": [],
+ "name": "denmark",
+ "shortname": ":flag_dk:",
+ "category": "flags",
+ "aliases": [":dk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "danmark", "dk"]
+ },
+ "flag_dm": {
+ "unicode": "1F1E9-1F1F2",
+ "unicode_alternates": [],
+ "name": "dominica",
+ "shortname": ":flag_dm:",
+ "category": "flags",
+ "aliases": [":dm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "dm"]
+ },
+ "flag_do": {
+ "unicode": "1F1E9-1F1F4",
+ "unicode_alternates": [],
+ "name": "the dominican republic",
+ "shortname": ":flag_do:",
+ "category": "flags",
+ "aliases": [":do:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "do"]
+ },
+ "flag_dz": {
+ "unicode": "1F1E9-1F1FF",
+ "unicode_alternates": [],
+ "name": "algeria",
+ "shortname": ":flag_dz:",
+ "category": "flags",
+ "aliases": [":dz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al jaza'ir", "al jazair", "dz"]
+ },
+ "flag_ec": {
+ "unicode": "1F1EA-1F1E8",
+ "unicode_alternates": [],
+ "name": "ecuador",
+ "shortname": ":flag_ec:",
+ "category": "flags",
+ "aliases": [":ec:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ec"]
+ },
+ "flag_ee": {
+ "unicode": "1F1EA-1F1EA",
+ "unicode_alternates": [],
+ "name": "estonia",
+ "shortname": ":flag_ee:",
+ "category": "flags",
+ "aliases": [":ee:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "eesti vabariik", "ee"]
+ },
+ "flag_eg": {
+ "unicode": "1F1EA-1F1EC",
+ "unicode_alternates": [],
+ "name": "egypt",
+ "shortname": ":flag_eg:",
+ "category": "flags",
+ "aliases": [":eg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "misr", "eg"]
+ },
+ "flag_eh": {
+ "unicode": "1F1EA-1F1ED",
+ "unicode_alternates": [],
+ "name": "western sahara",
+ "shortname": ":flag_eh:",
+ "category": "flags",
+ "aliases": [":eh:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "aṣ-Ṣaḥrā’ al-gharbīyah", "sahra", "gharbiyah", "eh"]
+ },
+ "flag_er": {
+ "unicode": "1F1EA-1F1F7",
+ "unicode_alternates": [],
+ "name": "eritrea",
+ "shortname": ":flag_er:",
+ "category": "flags",
+ "aliases": [":er:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "hagere ertra", "er"]
+ },
+ "flag_es": {
+ "unicode": "1F1EA-1F1F8",
+ "unicode_alternates": [],
+ "name": "spain",
+ "shortname": ":flag_es:",
+ "category": "flags",
+ "aliases": [":es:"],
+ "aliases_ascii": [],
+ "keywords": ["nation", "espa&ntilde;a", "country", "espana", "es"]
+ },
+ "flag_et": {
+ "unicode": "1F1EA-1F1F9",
+ "unicode_alternates": [],
+ "name": "ethiopia",
+ "shortname": ":flag_et:",
+ "category": "flags",
+ "aliases": [":et:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ityop'iya", "ityopiya", "et"]
+ },
+ "flag_fi": {
+ "unicode": "1F1EB-1F1EE",
+ "unicode_alternates": [],
+ "name": "finland",
+ "shortname": ":flag_fi:",
+ "category": "flags",
+ "aliases": [":fi:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "suomen tasavalta", "fi"]
+ },
+ "flag_fj": {
+ "unicode": "1F1EB-1F1EF",
+ "unicode_alternates": [],
+ "name": "fiji",
+ "shortname": ":flag_fj:",
+ "category": "flags",
+ "aliases": [":fj:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "fj"]
+ },
+ "flag_fk": {
+ "unicode": "1F1EB-1F1F0",
+ "unicode_alternates": [],
+ "name": "falkland islands",
+ "shortname": ":flag_fk:",
+ "category": "flags",
+ "aliases": [":fk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "islas malvinas", "fk"]
+ },
+ "flag_fm": {
+ "unicode": "1F1EB-1F1F2",
+ "unicode_alternates": [],
+ "name": "micronesia",
+ "shortname": ":flag_fm:",
+ "category": "flags",
+ "aliases": [":fm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "fm"]
+ },
+ "flag_fo": {
+ "unicode": "1F1EB-1F1F4",
+ "unicode_alternates": [],
+ "name": "faroe islands",
+ "shortname": ":flag_fo:",
+ "category": "flags",
+ "aliases": [":fo:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "foroyar", "fo"]
+ },
+ "flag_fr": {
+ "unicode": "1F1EB-1F1F7",
+ "unicode_alternates": [],
+ "name": "france",
+ "shortname": ":flag_fr:",
+ "category": "flags",
+ "aliases": [":fr:"],
+ "aliases_ascii": [],
+ "keywords": ["french", "nation", "country", "fr"]
+ },
+ "flag_ga": {
+ "unicode": "1F1EC-1F1E6",
+ "unicode_alternates": [],
+ "name": "gabon",
+ "shortname": ":flag_ga:",
+ "category": "flags",
+ "aliases": [":ga:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ga"]
+ },
+ "flag_gb": {
+ "unicode": "1F1EC-1F1E7",
+ "unicode_alternates": [],
+ "name": "great britain",
+ "shortname": ":flag_gb:",
+ "category": "flags",
+ "aliases": [":gb:"],
+ "aliases_ascii": [],
+ "keywords": ["UK", "gb", "britsh", "nation", "united kingdom", "england", "country"]
+ },
+ "flag_gd": {
+ "unicode": "1F1EC-1F1E9",
+ "unicode_alternates": [],
+ "name": "grenada",
+ "shortname": ":flag_gd:",
+ "category": "flags",
+ "aliases": [":gd:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gd"]
+ },
+ "flag_ge": {
+ "unicode": "1F1EC-1F1EA",
+ "unicode_alternates": [],
+ "name": "georgia",
+ "shortname": ":flag_ge:",
+ "category": "flags",
+ "aliases": [":ge:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sak'art'velo", "sakartvelo", "ge"]
+ },
+ "flag_gh": {
+ "unicode": "1F1EC-1F1ED",
+ "unicode_alternates": [],
+ "name": "ghana",
+ "shortname": ":flag_gh:",
+ "category": "flags",
+ "aliases": [":gh:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gh"]
+ },
+ "flag_gi": {
+ "unicode": "1F1EC-1F1EE",
+ "unicode_alternates": [],
+ "name": "gibraltar",
+ "shortname": ":flag_gi:",
+ "category": "flags",
+ "aliases": [":gi:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gi"]
+ },
+ "flag_gl": {
+ "unicode": "1F1EC-1F1F1",
+ "unicode_alternates": [],
+ "name": "greenland",
+ "shortname": ":flag_gl:",
+ "category": "flags",
+ "aliases": [":gl:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kalaallit nunaat", "gl"]
+ },
+ "flag_gm": {
+ "unicode": "1F1EC-1F1F2",
+ "unicode_alternates": [],
+ "name": "the gambia",
+ "shortname": ":flag_gm:",
+ "category": "flags",
+ "aliases": [":gm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gm"]
+ },
+ "flag_gn": {
+ "unicode": "1F1EC-1F1F3",
+ "unicode_alternates": [],
+ "name": "guinea",
+ "shortname": ":flag_gn:",
+ "category": "flags",
+ "aliases": [":gn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "guinee", "gn"]
+ },
+ "flag_gq": {
+ "unicode": "1F1EC-1F1F6",
+ "unicode_alternates": [],
+ "name": "equatorial guinea",
+ "shortname": ":flag_gq:",
+ "category": "flags",
+ "aliases": [":gq:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "guinea ecuatorial", "gq"]
+ },
+ "flag_gr": {
+ "unicode": "1F1EC-1F1F7",
+ "unicode_alternates": [],
+ "name": "greece",
+ "shortname": ":flag_gr:",
+ "category": "flags",
+ "aliases": [":gr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ellas", "ellada", "gr"]
+ },
+ "flag_gt": {
+ "unicode": "1F1EC-1F1F9",
+ "unicode_alternates": [],
+ "name": "guatemala",
+ "shortname": ":flag_gt:",
+ "category": "flags",
+ "aliases": [":gt:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gt"]
+ },
+ "flag_gu": {
+ "unicode": "1F1EC-1F1FA",
+ "unicode_alternates": [],
+ "name": "guam",
+ "shortname": ":flag_gu:",
+ "category": "flags",
+ "aliases": [":gu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gu"]
+ },
+ "flag_gw": {
+ "unicode": "1F1EC-1F1FC",
+ "unicode_alternates": [],
+ "name": "guinea-bissau",
+ "shortname": ":flag_gw:",
+ "category": "flags",
+ "aliases": [":gw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "guine-bissau", "guine bissau", "gw"]
+ },
+ "flag_gy": {
+ "unicode": "1F1EC-1F1FE",
+ "unicode_alternates": [],
+ "name": "guyana",
+ "shortname": ":flag_gy:",
+ "category": "flags",
+ "aliases": [":gy:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "gy"]
+ },
+ "flag_hk": {
+ "unicode": "1F1ED-1F1F0",
+ "unicode_alternates": [],
+ "name": "hong kong",
+ "shortname": ":flag_hk:",
+ "category": "flags",
+ "aliases": [":hk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "xianggang", "hk"]
+ },
+ "flag_hn": {
+ "unicode": "1F1ED-1F1F3",
+ "unicode_alternates": [],
+ "name": "honduras",
+ "shortname": ":flag_hn:",
+ "category": "flags",
+ "aliases": [":hn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "hn"]
+ },
+ "flag_hr": {
+ "unicode": "1F1ED-1F1F7",
+ "unicode_alternates": [],
+ "name": "croatia",
+ "shortname": ":flag_hr:",
+ "category": "flags",
+ "aliases": [":hr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "hrvatska", "hr"]
+ },
+ "flag_ht": {
+ "unicode": "1F1ED-1F1F9",
+ "unicode_alternates": [],
+ "name": "haiti",
+ "shortname": ":flag_ht:",
+ "category": "flags",
+ "aliases": [":ht:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ht"]
+ },
+ "flag_hu": {
+ "unicode": "1F1ED-1F1FA",
+ "unicode_alternates": [],
+ "name": "hungary",
+ "shortname": ":flag_hu:",
+ "category": "flags",
+ "aliases": [":hu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "magyarorszag", "hu"]
+ },
+ "flag_id": {
+ "unicode": "1F1EE-1F1E9",
+ "unicode_alternates": [],
+ "name": "indonesia",
+ "shortname": ":flag_id:",
+ "category": "flags",
+ "aliases": [":indonesia:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "id"]
+ },
+ "flag_ie": {
+ "unicode": "1F1EE-1F1EA",
+ "unicode_alternates": [],
+ "name": "ireland",
+ "shortname": ":flag_ie:",
+ "category": "flags",
+ "aliases": [":ie:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "&eacute;ire", "eire", "ie"]
+ },
+ "flag_il": {
+ "unicode": "1F1EE-1F1F1",
+ "unicode_alternates": [],
+ "name": "israel",
+ "shortname": ":flag_il:",
+ "category": "flags",
+ "aliases": [":il:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "yisra'el", "yisrael", "il"]
+ },
+ "flag_in": {
+ "unicode": "1F1EE-1F1F3",
+ "unicode_alternates": [],
+ "name": "india",
+ "shortname": ":flag_in:",
+ "category": "flags",
+ "aliases": [":in:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "bharat", "in"]
+ },
+ "flag_iq": {
+ "unicode": "1F1EE-1F1F6",
+ "unicode_alternates": [],
+ "name": "iraq",
+ "shortname": ":flag_iq:",
+ "category": "flags",
+ "aliases": [":iq:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "iq"]
+ },
+ "flag_ir": {
+ "unicode": "1F1EE-1F1F7",
+ "unicode_alternates": [],
+ "name": "iran",
+ "shortname": ":flag_ir:",
+ "category": "flags",
+ "aliases": [":ir:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ir"]
+ },
+ "flag_is": {
+ "unicode": "1F1EE-1F1F8",
+ "unicode_alternates": [],
+ "name": "iceland",
+ "shortname": ":flag_is:",
+ "category": "flags",
+ "aliases": [":is:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "lyoveldio island", "is"]
+ },
+ "flag_it": {
+ "unicode": "1F1EE-1F1F9",
+ "unicode_alternates": [],
+ "name": "italy",
+ "shortname": ":flag_it:",
+ "category": "flags",
+ "aliases": [":it:"],
+ "aliases_ascii": [],
+ "keywords": ["italia", "country", "nation", "it"]
+ },
+ "flag_je": {
+ "unicode": "1F1EF-1F1EA",
+ "unicode_alternates": [],
+ "name": "jersey",
+ "shortname": ":flag_je:",
+ "category": "flags",
+ "aliases": [":je:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "je"]
+ },
+ "flag_jm": {
+ "unicode": "1F1EF-1F1F2",
+ "unicode_alternates": [],
+ "name": "jamaica",
+ "shortname": ":flag_jm:",
+ "category": "flags",
+ "aliases": [":jm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "jm"]
+ },
+ "flag_jo": {
+ "unicode": "1F1EF-1F1F4",
+ "unicode_alternates": [],
+ "name": "jordan",
+ "shortname": ":flag_jo:",
+ "category": "flags",
+ "aliases": [":jo:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al urdun", "jo"]
+ },
+ "flag_jp": {
+ "unicode": "1F1EF-1F1F5",
+ "unicode_alternates": [],
+ "name": "japan",
+ "shortname": ":flag_jp:",
+ "category": "flags",
+ "aliases": [":jp:"],
+ "aliases_ascii": [],
+ "keywords": ["nation", "nippon", "country", "jp"]
+ },
+ "flag_ke": {
+ "unicode": "1F1F0-1F1EA",
+ "unicode_alternates": [],
+ "name": "kenya",
+ "shortname": ":flag_ke:",
+ "category": "flags",
+ "aliases": [":ke:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ke"]
+ },
+ "flag_kg": {
+ "unicode": "1F1F0-1F1EC",
+ "unicode_alternates": [],
+ "name": "kyrgyzstan",
+ "shortname": ":flag_kg:",
+ "category": "flags",
+ "aliases": [":kg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kyrgyz respublikasy", "kg"]
+ },
+ "flag_kh": {
+ "unicode": "1F1F0-1F1ED",
+ "unicode_alternates": [],
+ "name": "cambodia",
+ "shortname": ":flag_kh:",
+ "category": "flags",
+ "aliases": [":kh:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kampuchea", "kh"]
+ },
+ "flag_ki": {
+ "unicode": "1F1F0-1F1EE",
+ "unicode_alternates": [],
+ "name": "kiribati",
+ "shortname": ":flag_ki:",
+ "category": "flags",
+ "aliases": [":ki:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kiribati", "kiribas", "ki"]
+ },
+ "flag_km": {
+ "unicode": "1F1F0-1F1F2",
+ "unicode_alternates": [],
+ "name": "the comoros",
+ "shortname": ":flag_km:",
+ "category": "flags",
+ "aliases": [":km:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "km"]
+ },
+ "flag_kn": {
+ "unicode": "1F1F0-1F1F3",
+ "unicode_alternates": [],
+ "name": "saint kitts and nevis",
+ "shortname": ":flag_kn:",
+ "category": "flags",
+ "aliases": [":kn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kn"]
+ },
+ "flag_kp": {
+ "unicode": "1F1F0-1F1F5",
+ "unicode_alternates": [],
+ "name": "north korea",
+ "shortname": ":flag_kp:",
+ "category": "flags",
+ "aliases": [":kp:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "kp"]
+ },
+ "flag_kr": {
+ "unicode": "1F1F0-1F1F7",
+ "unicode_alternates": [],
+ "name": "korea",
+ "shortname": ":flag_kr:",
+ "category": "flags",
+ "aliases": [":kr:"],
+ "aliases_ascii": [],
+ "keywords": ["nation", "country", "south korea", "kr"]
+ },
+ "flag_kw": {
+ "unicode": "1F1F0-1F1FC",
+ "unicode_alternates": [],
+ "name": "kuwait",
+ "shortname": ":flag_kw:",
+ "category": "flags",
+ "aliases": [":kw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al kuwayt", "kw"]
+ },
+ "flag_ky": {
+ "unicode": "1F1F0-1F1FE",
+ "unicode_alternates": [],
+ "name": "cayman islands",
+ "shortname": ":flag_ky:",
+ "category": "flags",
+ "aliases": [":ky:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ky"]
+ },
+ "flag_kz": {
+ "unicode": "1F1F0-1F1FF",
+ "unicode_alternates": [],
+ "name": "kazakhstan",
+ "shortname": ":flag_kz:",
+ "category": "flags",
+ "aliases": [":kz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "qazaqstan", "kz"]
+ },
+ "flag_la": {
+ "unicode": "1F1F1-1F1E6",
+ "unicode_alternates": [],
+ "name": "laos",
+ "shortname": ":flag_la:",
+ "category": "flags",
+ "aliases": [":la:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "la"]
+ },
+ "flag_lb": {
+ "unicode": "1F1F1-1F1E7",
+ "unicode_alternates": [],
+ "name": "lebanon",
+ "shortname": ":flag_lb:",
+ "category": "flags",
+ "aliases": [":lb:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "lubnan", "lb"]
+ },
+ "flag_lc": {
+ "unicode": "1F1F1-1F1E8",
+ "unicode_alternates": [],
+ "name": "saint lucia",
+ "shortname": ":flag_lc:",
+ "category": "flags",
+ "aliases": [":lc:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "lc"]
+ },
+ "flag_li": {
+ "unicode": "1F1F1-1F1EE",
+ "unicode_alternates": [],
+ "name": "liechtenstein",
+ "shortname": ":flag_li:",
+ "category": "flags",
+ "aliases": [":li:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "li"]
+ },
+ "flag_lk": {
+ "unicode": "1F1F1-1F1F0",
+ "unicode_alternates": [],
+ "name": "sri lanka",
+ "shortname": ":flag_lk:",
+ "category": "flags",
+ "aliases": [":lk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "lk"]
+ },
+ "flag_lr": {
+ "unicode": "1F1F1-1F1F7",
+ "unicode_alternates": [],
+ "name": "liberia",
+ "shortname": ":flag_lr:",
+ "category": "flags",
+ "aliases": [":lr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "lr"]
+ },
+ "flag_ls": {
+ "unicode": "1F1F1-1F1F8",
+ "unicode_alternates": [],
+ "name": "lesotho",
+ "shortname": ":flag_ls:",
+ "category": "flags",
+ "aliases": [":ls:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ls"]
+ },
+ "flag_lt": {
+ "unicode": "1F1F1-1F1F9",
+ "unicode_alternates": [],
+ "name": "lithuania",
+ "shortname": ":flag_lt:",
+ "category": "flags",
+ "aliases": [":lt:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "lietuva", "lt"]
+ },
+ "flag_lu": {
+ "unicode": "1F1F1-1F1FA",
+ "unicode_alternates": [],
+ "name": "luxembourg",
+ "shortname": ":flag_lu:",
+ "category": "flags",
+ "aliases": [":lu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "luxembourg", "letzebuerg", "lu"]
+ },
+ "flag_lv": {
+ "unicode": "1F1F1-1F1FB",
+ "unicode_alternates": [],
+ "name": "latvia",
+ "shortname": ":flag_lv:",
+ "category": "flags",
+ "aliases": [":lv:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "latvija", "lv"]
+ },
+ "flag_ly": {
+ "unicode": "1F1F1-1F1FE",
+ "unicode_alternates": [],
+ "name": "libya",
+ "shortname": ":flag_ly:",
+ "category": "flags",
+ "aliases": [":ly:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "libiyah", "ly"]
+ },
+ "flag_ma": {
+ "unicode": "1F1F2-1F1E6",
+ "unicode_alternates": [],
+ "name": "morocco",
+ "shortname": ":flag_ma:",
+ "category": "flags",
+ "aliases": [":ma:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al maghrib", "ma"]
+ },
+ "flag_mc": {
+ "unicode": "1F1F2-1F1E8",
+ "unicode_alternates": [],
+ "name": "monaco",
+ "shortname": ":flag_mc:",
+ "category": "flags",
+ "aliases": [":mc:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mc"]
+ },
+ "flag_md": {
+ "unicode": "1F1F2-1F1E9",
+ "unicode_alternates": [],
+ "name": "moldova",
+ "shortname": ":flag_md:",
+ "category": "flags",
+ "aliases": [":md:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "md"]
+ },
+ "flag_me": {
+ "unicode": "1F1F2-1F1EA",
+ "unicode_alternates": [],
+ "name": "montenegro",
+ "shortname": ":flag_me:",
+ "category": "flags",
+ "aliases": [":me:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "crna gora", "me"]
+ },
+ "flag_mg": {
+ "unicode": "1F1F2-1F1EC",
+ "unicode_alternates": [],
+ "name": "madagascar",
+ "shortname": ":flag_mg:",
+ "category": "flags",
+ "aliases": [":mg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mg"]
+ },
+ "flag_mh": {
+ "unicode": "1F1F2-1F1ED",
+ "unicode_alternates": [],
+ "name": "the marshall islands",
+ "shortname": ":flag_mh:",
+ "category": "flags",
+ "aliases": [":mh:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mh"]
+ },
+ "flag_mk": {
+ "unicode": "1F1F2-1F1F0",
+ "unicode_alternates": [],
+ "name": "macedonia",
+ "shortname": ":flag_mk:",
+ "category": "flags",
+ "aliases": [":mk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mk"]
+ },
+ "flag_ml": {
+ "unicode": "1F1F2-1F1F1",
+ "unicode_alternates": [],
+ "name": "mali",
+ "shortname": ":flag_ml:",
+ "category": "flags",
+ "aliases": [":ml:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ml"]
+ },
+ "flag_mm": {
+ "unicode": "1F1F2-1F1F2",
+ "unicode_alternates": [],
+ "name": "myanmar",
+ "shortname": ":flag_mm:",
+ "category": "flags",
+ "aliases": [":mm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "myanma naingngandaw", "mm"]
+ },
+ "flag_mn": {
+ "unicode": "1F1F2-1F1F3",
+ "unicode_alternates": [],
+ "name": "mongolia",
+ "shortname": ":flag_mn:",
+ "category": "flags",
+ "aliases": [":mn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mongol uls", "mn"]
+ },
+ "flag_mo": {
+ "unicode": "1F1F2-1F1F4",
+ "unicode_alternates": [],
+ "name": "macau",
+ "shortname": ":flag_mo:",
+ "category": "flags",
+ "aliases": [":mo:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "aomen", "mo"]
+ },
+ "flag_mr": {
+ "unicode": "1F1F2-1F1F7",
+ "unicode_alternates": [],
+ "name": "mauritania",
+ "shortname": ":flag_mr:",
+ "category": "flags",
+ "aliases": [":mr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "muritaniyah", "mr"]
+ },
+ "flag_ms": {
+ "unicode": "1F1F2-1F1F8",
+ "unicode_alternates": [],
+ "name": "montserrat",
+ "shortname": ":flag_ms:",
+ "category": "flags",
+ "aliases": [":ms:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ms"]
+ },
+ "flag_mt": {
+ "unicode": "1F1F2-1F1F9",
+ "unicode_alternates": [],
+ "name": "malta",
+ "shortname": ":flag_mt:",
+ "category": "flags",
+ "aliases": [":mt:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mt"]
+ },
+ "flag_mu": {
+ "unicode": "1F1F2-1F1FA",
+ "unicode_alternates": [],
+ "name": "mauritius",
+ "shortname": ":flag_mu:",
+ "category": "flags",
+ "aliases": [":mu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mu"]
+ },
+ "flag_mv": {
+ "unicode": "1F1F2-1F1FB",
+ "unicode_alternates": [],
+ "name": "maldives",
+ "shortname": ":flag_mv:",
+ "category": "flags",
+ "aliases": [":mv:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "dhivehi raajje", "mv"]
+ },
+ "flag_mw": {
+ "unicode": "1F1F2-1F1FC",
+ "unicode_alternates": [],
+ "name": "malawi",
+ "shortname": ":flag_mw:",
+ "category": "flags",
+ "aliases": [":mw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mw"]
+ },
+ "flag_mx": {
+ "unicode": "1F1F2-1F1FD",
+ "unicode_alternates": [],
+ "name": "mexico",
+ "shortname": ":flag_mx:",
+ "category": "flags",
+ "aliases": [":mx:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mx"]
+ },
+ "flag_my": {
+ "unicode": "1F1F2-1F1FE",
+ "unicode_alternates": [],
+ "name": "malaysia",
+ "shortname": ":flag_my:",
+ "category": "flags",
+ "aliases": [":my:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "my"]
+ },
+ "flag_mz": {
+ "unicode": "1F1F2-1F1FF",
+ "unicode_alternates": [],
+ "name": "mozambique",
+ "shortname": ":flag_mz:",
+ "category": "flags",
+ "aliases": [":mz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "mocambique", "mz"]
+ },
+ "flag_na": {
+ "unicode": "1F1F3-1F1E6",
+ "unicode_alternates": [],
+ "name": "namibia",
+ "shortname": ":flag_na:",
+ "category": "flags",
+ "aliases": [":na:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "na"]
+ },
+ "flag_nc": {
+ "unicode": "1F1F3-1F1E8",
+ "unicode_alternates": [],
+ "name": "new caledonia",
+ "shortname": ":flag_nc:",
+ "category": "flags",
+ "aliases": [":nc:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "nouvelle", "cal&eacute;donie", "caledonie", "nc"]
+ },
+ "flag_ne": {
+ "unicode": "1F1F3-1F1EA",
+ "unicode_alternates": [],
+ "name": "niger",
+ "shortname": ":flag_ne:",
+ "category": "flags",
+ "aliases": [":ne:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ne"]
+ },
+ "flag_ng": {
+ "unicode": "1F1F3-1F1EC",
+ "unicode_alternates": [],
+ "name": "nigeria",
+ "shortname": ":flag_ng:",
+ "category": "flags",
+ "aliases": [":nigeria:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ng"]
+ },
+ "flag_ni": {
+ "unicode": "1F1F3-1F1EE",
+ "unicode_alternates": [],
+ "name": "nicaragua",
+ "shortname": ":flag_ni:",
+ "category": "flags",
+ "aliases": [":ni:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ni"]
+ },
+ "flag_nl": {
+ "unicode": "1F1F3-1F1F1",
+ "unicode_alternates": [],
+ "name": "the netherlands",
+ "shortname": ":flag_nl:",
+ "category": "flags",
+ "aliases": [":nl:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "nederland", "holland", "nl"]
+ },
+ "flag_no": {
+ "unicode": "1F1F3-1F1F4",
+ "unicode_alternates": [],
+ "name": "norway",
+ "shortname": ":flag_no:",
+ "category": "flags",
+ "aliases": [":no:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "norge", "no"]
+ },
+ "flag_np": {
+ "unicode": "1F1F3-1F1F5",
+ "unicode_alternates": [],
+ "name": "nepal",
+ "shortname": ":flag_np:",
+ "category": "flags",
+ "aliases": [":np:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "np"]
+ },
+ "flag_nr": {
+ "unicode": "1F1F3-1F1F7",
+ "unicode_alternates": [],
+ "name": "nauru",
+ "shortname": ":flag_nr:",
+ "category": "flags",
+ "aliases": [":nr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "nr"]
+ },
+ "flag_nu": {
+ "unicode": "1F1F3-1F1FA",
+ "unicode_alternates": [],
+ "name": "niue",
+ "shortname": ":flag_nu:",
+ "category": "flags",
+ "aliases": [":nu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "nu"]
+ },
+ "flag_nz": {
+ "unicode": "1F1F3-1F1FF",
+ "unicode_alternates": [],
+ "name": "new zealand",
+ "shortname": ":flag_nz:",
+ "category": "flags",
+ "aliases": [":nz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "aotearoa", "nz"]
+ },
+ "flag_om": {
+ "unicode": "1F1F4-1F1F2",
+ "unicode_alternates": [],
+ "name": "oman",
+ "shortname": ":flag_om:",
+ "category": "flags",
+ "aliases": [":om:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "saltanat uman", "om"]
+ },
+ "flag_pa": {
+ "unicode": "1F1F5-1F1E6",
+ "unicode_alternates": [],
+ "name": "panama",
+ "shortname": ":flag_pa:",
+ "category": "flags",
+ "aliases": [":pa:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "pa"]
+ },
+ "flag_pe": {
+ "unicode": "1F1F5-1F1EA",
+ "unicode_alternates": [],
+ "name": "peru",
+ "shortname": ":flag_pe:",
+ "category": "flags",
+ "aliases": [":pe:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "pe"]
+ },
+ "flag_pf": {
+ "unicode": "1F1F5-1F1EB",
+ "unicode_alternates": [],
+ "name": "french polynesia",
+ "shortname": ":flag_pf:",
+ "category": "flags",
+ "aliases": [":pf:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "polyn&eacute;sie fran&ccedil;aise", "polynesie francaise", "pf"]
+ },
+ "flag_pg": {
+ "unicode": "1F1F5-1F1EC",
+ "unicode_alternates": [],
+ "name": "papua new guinea",
+ "shortname": ":flag_pg:",
+ "category": "flags",
+ "aliases": [":pg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "papua niu gini", "pg"]
+ },
+ "flag_ph": {
+ "unicode": "1F1F5-1F1ED",
+ "unicode_alternates": [],
+ "name": "the philippines",
+ "shortname": ":flag_ph:",
+ "category": "flags",
+ "aliases": [":ph:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "pilipinas", "ph"]
+ },
+ "flag_pk": {
+ "unicode": "1F1F5-1F1F0",
+ "unicode_alternates": [],
+ "name": "pakistan",
+ "shortname": ":flag_pk:",
+ "category": "flags",
+ "aliases": [":pk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "pk"]
+ },
+ "flag_pl": {
+ "unicode": "1F1F5-1F1F1",
+ "unicode_alternates": [],
+ "name": "poland",
+ "shortname": ":flag_pl:",
+ "category": "flags",
+ "aliases": [":pl:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "polska", "pl"]
+ },
+ "flag_pr": {
+ "unicode": "1F1F5-1F1F7",
+ "unicode_alternates": [],
+ "name": "puerto rico",
+ "shortname": ":flag_pr:",
+ "category": "flags",
+ "aliases": [":pr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "pr"]
+ },
+ "flag_ps": {
+ "unicode": "1F1F5-1F1F8",
+ "unicode_alternates": [],
+ "name": "palestinian authority",
+ "shortname": ":flag_ps:",
+ "category": "flags",
+ "aliases": [":ps:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ps"]
+ },
+ "flag_pt": {
+ "unicode": "1F1F5-1F1F9",
+ "unicode_alternates": [],
+ "name": "portugal",
+ "shortname": ":flag_pt:",
+ "category": "flags",
+ "aliases": [":pt:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "pt"]
+ },
+ "flag_pw": {
+ "unicode": "1F1F5-1F1FC",
+ "unicode_alternates": [],
+ "name": "palau",
+ "shortname": ":flag_pw:",
+ "category": "flags",
+ "aliases": [":pw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "belau", "pw"]
+ },
+ "flag_py": {
+ "unicode": "1F1F5-1F1FE",
+ "unicode_alternates": [],
+ "name": "paraguay",
+ "shortname": ":flag_py:",
+ "category": "flags",
+ "aliases": [":py:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "py"]
+ },
+ "flag_qa": {
+ "unicode": "1F1F6-1F1E6",
+ "unicode_alternates": [],
+ "name": "qatar",
+ "shortname": ":flag_qa:",
+ "category": "flags",
+ "aliases": [":qa:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "dawlat qatar", "qa"]
+ },
+ "flag_ro": {
+ "unicode": "1F1F7-1F1F4",
+ "unicode_alternates": [],
+ "name": "romania",
+ "shortname": ":flag_ro:",
+ "category": "flags",
+ "aliases": [":ro:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ro"]
+ },
+ "flag_rs": {
+ "unicode": "1F1F7-1F1F8",
+ "unicode_alternates": [],
+ "name": "serbia",
+ "shortname": ":flag_rs:",
+ "category": "flags",
+ "aliases": [":rs:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "srbija", "rs"]
+ },
+ "flag_ru": {
+ "unicode": "1F1F7-1F1FA",
+ "unicode_alternates": [],
+ "name": "russia",
+ "shortname": ":flag_ru:",
+ "category": "flags",
+ "aliases": [":ru:"],
+ "aliases_ascii": [],
+ "keywords": ["nation", "russian", "country", "ru"]
+ },
+ "flag_rw": {
+ "unicode": "1F1F7-1F1FC",
+ "unicode_alternates": [],
+ "name": "rwanda",
+ "shortname": ":flag_rw:",
+ "category": "flags",
+ "aliases": [":rw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "rw"]
+ },
+ "flag_sa": {
+ "unicode": "1F1F8-1F1E6",
+ "unicode_alternates": [],
+ "name": "saudi arabia",
+ "shortname": ":flag_sa:",
+ "category": "flags",
+ "aliases": [":saudiarabia:", ":saudi:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al arabiyah as suudiyah", "sa"]
+ },
+ "flag_sb": {
+ "unicode": "1F1F8-1F1E7",
+ "unicode_alternates": [],
+ "name": "the solomon islands",
+ "shortname": ":flag_sb:",
+ "category": "flags",
+ "aliases": [":sb:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sb"]
+ },
+ "flag_sc": {
+ "unicode": "1F1F8-1F1E8",
+ "unicode_alternates": [],
+ "name": "the seychelles",
+ "shortname": ":flag_sc:",
+ "category": "flags",
+ "aliases": [":sc:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "seychelles", "sc"]
+ },
+ "flag_sd": {
+ "unicode": "1F1F8-1F1E9",
+ "unicode_alternates": [],
+ "name": "sudan",
+ "shortname": ":flag_sd:",
+ "category": "flags",
+ "aliases": [":sd:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "as-sudan", "sd"]
+ },
+ "flag_se": {
+ "unicode": "1F1F8-1F1EA",
+ "unicode_alternates": [],
+ "name": "sweden",
+ "shortname": ":flag_se:",
+ "category": "flags",
+ "aliases": [":se:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sverige", "se"]
+ },
+ "flag_sg": {
+ "unicode": "1F1F8-1F1EC",
+ "unicode_alternates": [],
+ "name": "singapore",
+ "shortname": ":flag_sg:",
+ "category": "flags",
+ "aliases": [":sg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sg"]
+ },
+ "flag_sh": {
+ "unicode": "1F1F8-1F1ED",
+ "unicode_alternates": [],
+ "name": "saint helena",
+ "shortname": ":flag_sh:",
+ "category": "flags",
+ "aliases": [":sh:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sh"]
+ },
+ "flag_si": {
+ "unicode": "1F1F8-1F1EE",
+ "unicode_alternates": [],
+ "name": "slovenia",
+ "shortname": ":flag_si:",
+ "category": "flags",
+ "aliases": [":si:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "slovenija", "si"]
+ },
+ "flag_sk": {
+ "unicode": "1F1F8-1F1F0",
+ "unicode_alternates": [],
+ "name": "slovakia",
+ "shortname": ":flag_sk:",
+ "category": "flags",
+ "aliases": [":sk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sk"]
+ },
+ "flag_sl": {
+ "unicode": "1F1F8-1F1F1",
+ "unicode_alternates": [],
+ "name": "sierra leone",
+ "shortname": ":flag_sl:",
+ "category": "flags",
+ "aliases": [":sl:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sl"]
+ },
+ "flag_sm": {
+ "unicode": "1F1F8-1F1F2",
+ "unicode_alternates": [],
+ "name": "san marino",
+ "shortname": ":flag_sm:",
+ "category": "flags",
+ "aliases": [":sm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sm"]
+ },
+ "flag_sn": {
+ "unicode": "1F1F8-1F1F3",
+ "unicode_alternates": [],
+ "name": "senegal",
+ "shortname": ":flag_sn:",
+ "category": "flags",
+ "aliases": [":sn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sn"]
+ },
+ "flag_so": {
+ "unicode": "1F1F8-1F1F4",
+ "unicode_alternates": [],
+ "name": "somalia",
+ "shortname": ":flag_so:",
+ "category": "flags",
+ "aliases": [":so:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "so"]
+ },
+ "flag_sr": {
+ "unicode": "1F1F8-1F1F7",
+ "unicode_alternates": [],
+ "name": "suriname",
+ "shortname": ":flag_sr:",
+ "category": "flags",
+ "aliases": [":sr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sr"]
+ },
+ "flag_st": {
+ "unicode": "1F1F8-1F1F9",
+ "unicode_alternates": [],
+ "name": "sao tome and principe",
+ "shortname": ":flag_st:",
+ "category": "flags",
+ "aliases": [":st:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sao tome e principe", "st"]
+ },
+ "flag_sv": {
+ "unicode": "1F1F8-1F1FB",
+ "unicode_alternates": [],
+ "name": "el salvador",
+ "shortname": ":flag_sv:",
+ "category": "flags",
+ "aliases": [":sv:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sv"]
+ },
+ "flag_sy": {
+ "unicode": "1F1F8-1F1FE",
+ "unicode_alternates": [],
+ "name": "syria",
+ "shortname": ":flag_sy:",
+ "category": "flags",
+ "aliases": [":sy:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sy"]
+ },
+ "flag_sz": {
+ "unicode": "1F1F8-1F1FF",
+ "unicode_alternates": [],
+ "name": "swaziland",
+ "shortname": ":flag_sz:",
+ "category": "flags",
+ "aliases": [":sz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "sz"]
+ },
+ "flag_td": {
+ "unicode": "1F1F9-1F1E9",
+ "unicode_alternates": [],
+ "name": "chad",
+ "shortname": ":flag_td:",
+ "category": "flags",
+ "aliases": [":td:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tchad", "td"]
+ },
+ "flag_tg": {
+ "unicode": "1F1F9-1F1EC",
+ "unicode_alternates": [],
+ "name": "togo",
+ "shortname": ":flag_tg:",
+ "category": "flags",
+ "aliases": [":tg:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "republique togolaise", "tg"]
+ },
+ "flag_th": {
+ "unicode": "1F1F9-1F1ED",
+ "unicode_alternates": [],
+ "name": "thailand",
+ "shortname": ":flag_th:",
+ "category": "flags",
+ "aliases": [":th:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "prathet thai", "th"]
+ },
+ "flag_tj": {
+ "unicode": "1F1F9-1F1EF",
+ "unicode_alternates": [],
+ "name": "tajikistan",
+ "shortname": ":flag_tj:",
+ "category": "flags",
+ "aliases": [":tj:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "jumhurii tojikiston", "tj"]
+ },
+ "flag_tl": {
+ "unicode": "1F1F9-1F1F1",
+ "unicode_alternates": [],
+ "name": "east timor",
+ "shortname": ":flag_tl:",
+ "category": "flags",
+ "aliases": [":tl:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tl"]
+ },
+ "flag_tm": {
+ "unicode": "1F1F9-1F1F2",
+ "unicode_alternates": [],
+ "name": "turkmenistan",
+ "shortname": ":flag_tm:",
+ "category": "flags",
+ "aliases": [":turkmenistan:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tm"]
+ },
+ "flag_tn": {
+ "unicode": "1F1F9-1F1F3",
+ "unicode_alternates": [],
+ "name": "tunisia",
+ "shortname": ":flag_tn:",
+ "category": "flags",
+ "aliases": [":tn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tunis", "tn"]
+ },
+ "flag_to": {
+ "unicode": "1F1F9-1F1F4",
+ "unicode_alternates": [],
+ "name": "tonga",
+ "shortname": ":flag_to:",
+ "category": "flags",
+ "aliases": [":to:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "to"]
+ },
+ "flag_tr": {
+ "unicode": "1F1F9-1F1F7",
+ "unicode_alternates": [],
+ "name": "turkey",
+ "shortname": ":flag_tr:",
+ "category": "flags",
+ "aliases": [":tr:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "turkiye"]
+ },
+ "flag_tt": {
+ "unicode": "1F1F9-1F1F9",
+ "unicode_alternates": [],
+ "name": "trinidad and tobago",
+ "shortname": ":flag_tt:",
+ "category": "flags",
+ "aliases": [":tt:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tt"]
+ },
+ "flag_tv": {
+ "unicode": "1F1F9-1F1FB",
+ "unicode_alternates": [],
+ "name": "tuvalu",
+ "shortname": ":flag_tv:",
+ "category": "flags",
+ "aliases": [":tuvalu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tv"]
+ },
+ "flag_tw": {
+ "unicode": "1F1F9-1F1FC",
+ "unicode_alternates": [],
+ "name": "the republic of china",
+ "shortname": ":flag_tw:",
+ "category": "flags",
+ "aliases": [":tw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "taiwan", "tw"]
+ },
+ "flag_tz": {
+ "unicode": "1F1F9-1F1FF",
+ "unicode_alternates": [],
+ "name": "tanzania",
+ "shortname": ":flag_tz:",
+ "category": "flags",
+ "aliases": [":tz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "tz"]
+ },
+ "flag_ua": {
+ "unicode": "1F1FA-1F1E6",
+ "unicode_alternates": [],
+ "name": "ukraine",
+ "shortname": ":flag_ua:",
+ "category": "flags",
+ "aliases": [":ua:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ukrayina", "ua"]
+ },
+ "flag_ug": {
+ "unicode": "1F1FA-1F1EC",
+ "unicode_alternates": [],
+ "name": "uganda",
+ "shortname": ":flag_ug:",
+ "category": "flags",
+ "aliases": [":ug:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ug"]
+ },
+ "flag_us": {
+ "unicode": "1F1FA-1F1F8",
+ "unicode_alternates": [],
+ "name": "united states",
+ "shortname": ":flag_us:",
+ "category": "flags",
+ "aliases": [":us:"],
+ "aliases_ascii": [],
+ "keywords": ["american", "country", "nation", "usa", "united states of america", "america", "old glory", "us"]
+ },
+ "flag_uy": {
+ "unicode": "1F1FA-1F1FE",
+ "unicode_alternates": [],
+ "name": "uruguay",
+ "shortname": ":flag_uy:",
+ "category": "flags",
+ "aliases": [":uy:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "uy"]
+ },
+ "flag_uz": {
+ "unicode": "1F1FA-1F1FF",
+ "unicode_alternates": [],
+ "name": "uzbekistan",
+ "shortname": ":flag_uz:",
+ "category": "flags",
+ "aliases": [":uz:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "uzbekiston respublikasi", "uz"]
+ },
+ "flag_va": {
+ "unicode": "1F1FB-1F1E6",
+ "unicode_alternates": [],
+ "name": "the vatican city",
+ "shortname": ":flag_va:",
+ "category": "flags",
+ "aliases": [":va:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "va"]
+ },
+ "flag_vc": {
+ "unicode": "1F1FB-1F1E8",
+ "unicode_alternates": [],
+ "name": "saint vincent and the grenadines",
+ "shortname": ":flag_vc:",
+ "category": "flags",
+ "aliases": [":vc:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "vc"]
+ },
+ "flag_ve": {
+ "unicode": "1F1FB-1F1EA",
+ "unicode_alternates": [],
+ "name": "venezuela",
+ "shortname": ":flag_ve:",
+ "category": "flags",
+ "aliases": [":ve:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "ve"]
+ },
+ "flag_vi": {
+ "unicode": "1F1FB-1F1EE",
+ "unicode_alternates": [],
+ "name": "u.s. virgin islands",
+ "shortname": ":flag_vi:",
+ "category": "flags",
+ "aliases": [":vi:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "vi"]
+ },
+ "flag_vn": {
+ "unicode": "1F1FB-1F1F3",
+ "unicode_alternates": [],
+ "name": "vietnam",
+ "shortname": ":flag_vn:",
+ "category": "flags",
+ "aliases": [":vn:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "viet nam", "vn"]
+ },
+ "flag_vu": {
+ "unicode": "1F1FB-1F1FA",
+ "unicode_alternates": [],
+ "name": "vanuatu",
+ "shortname": ":flag_vu:",
+ "category": "flags",
+ "aliases": [":vu:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "vu"]
+ },
+ "flag_wf": {
+ "unicode": "1F1FC-1F1EB",
+ "unicode_alternates": [],
+ "name": "wallis and futuna",
+ "shortname": ":flag_wf:",
+ "category": "flags",
+ "aliases": [":wf:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "wf"]
+ },
+ "flag_white": {
+ "unicode": "1F3F3",
+ "unicode_alternates": [],
+ "name": "waving white flag",
+ "shortname": ":flag_white:",
+ "category": "objects_symbols",
+ "aliases": [":waving_white_flag:"],
+ "aliases_ascii": [],
+ "keywords": ["symbol", "signal"]
+ },
+ "flag_ws": {
+ "unicode": "1F1FC-1F1F8",
+ "unicode_alternates": [],
+ "name": "samoa",
+ "shortname": ":flag_ws:",
+ "category": "flags",
+ "aliases": [":ws:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "american samoa", "ws"]
+ },
+ "flag_xk": {
+ "unicode": "1F1FD-1F1F0",
+ "unicode_alternates": [],
+ "name": "kosovo",
+ "shortname": ":flag_xk:",
+ "category": "flags",
+ "aliases": [":xk:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "xk"]
+ },
+ "flag_ye": {
+ "unicode": "1F1FE-1F1EA",
+ "unicode_alternates": [],
+ "name": "yemen",
+ "shortname": ":flag_ye:",
+ "category": "flags",
+ "aliases": [":ye:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "al yaman", "ye"]
+ },
+ "flag_za": {
+ "unicode": "1F1FF-1F1E6",
+ "unicode_alternates": [],
+ "name": "south africa",
+ "shortname": ":flag_za:",
+ "category": "flags",
+ "aliases": [":za:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation"]
+ },
+ "flag_zm": {
+ "unicode": "1F1FF-1F1F2",
+ "unicode_alternates": [],
+ "name": "zambia",
+ "shortname": ":flag_zm:",
+ "category": "flags",
+ "aliases": [":zm:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "zm"]
+ },
+ "flag_zw": {
+ "unicode": "1F1FF-1F1FC",
+ "unicode_alternates": [],
+ "name": "zimbabwe",
+ "shortname": ":flag_zw:",
+ "category": "flags",
+ "aliases": [":zw:"],
+ "aliases_ascii": [],
+ "keywords": ["country", "nation", "zw"]
+ },
+ "flags": {
+ "unicode": "1F38F",
+ "unicode_alternates": [],
+ "name": "carp streamer",
+ "shortname": ":flags:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["banner", "carp", "fish", "japanese", "koinobori", "children", "kids", "boys", "celebration", "happiness", "carp", "streamers", "japanese", "holiday", "flags"],
+ "moji": "🎏"
+ },
+ "flashlight": {
+ "unicode": "1F526",
+ "unicode_alternates": [],
+ "name": "electric torch",
+ "shortname": ":flashlight:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dark"],
+ "moji": "🔦"
+ },
+ "flip_phone": {
+ "unicode": "1F581",
+ "unicode_alternates": [],
+ "name": "clamshell mobile phone",
+ "shortname": ":flip_phone:",
+ "category": "objects_symbols",
+ "aliases": [":clamshell_mobile_phone:"],
+ "aliases_ascii": [],
+ "keywords": ["cellphone"]
+ },
+ "floppy_black": {
+ "unicode": "1F5AA",
+ "unicode_alternates": [],
+ "name": "black hard shell floppy disk",
+ "shortname": ":floppy_black:",
+ "category": "objects_symbols",
+ "aliases": [":black_hard_shell_floppy_disk:"],
+ "aliases_ascii": [],
+ "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"]
+ },
+ "floppy_disk": {
+ "unicode": "1F4BE",
+ "unicode_alternates": [],
+ "name": "floppy disk",
+ "shortname": ":floppy_disk:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["oldschool", "save", "technology", "floppy", "disk", "storage", "information", "computer", "drive", "megabyte"],
+ "moji": "💾"
+ },
+ "floppy_white": {
+ "unicode": "1F5AB",
+ "unicode_alternates": [],
+ "name": "white hard shell floppy disk",
+ "shortname": ":floppy_white:",
+ "category": "objects_symbols",
+ "aliases": [":white_hard_shell_floppy_disk:"],
+ "aliases_ascii": [],
+ "keywords": ["oldschool", "save", "technology", "storage", "information", "computer", "drive", "megabyte"]
+ },
+ "flower_playing_cards": {
+ "unicode": "1F3B4",
+ "unicode_alternates": [],
+ "name": "flower playing cards",
+ "shortname": ":flower_playing_cards:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["playing", "card", "flower", "game", "august", "moon", "special"],
+ "moji": "🎴"
+ },
+ "flushed": {
+ "unicode": "1F633",
+ "unicode_alternates": [],
+ "name": "flushed face",
+ "shortname": ":flushed:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":$", "=$"],
+ "keywords": ["blush", "face", "flattered", "flush", "blush", "red", "pink", "cheeks", "shy"],
+ "moji": "😳"
+ },
+ "fog": {
+ "unicode": "1F32B",
+ "unicode_alternates": [],
+ "name": "fog",
+ "shortname": ":fog:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["weather", "damp", "cloud", "hazy"]
+ },
+ "foggy": {
+ "unicode": "1F301",
+ "unicode_alternates": [],
+ "name": "foggy",
+ "shortname": ":foggy:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mountain", "photo", "bridge", "weather", "fog", "foggy"],
+ "moji": "🌁"
+ },
+ "folder": {
+ "unicode": "1F5C0",
+ "unicode_alternates": [],
+ "name": "folder",
+ "shortname": ":folder:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents"]
+ },
+ "folder_open": {
+ "unicode": "1F5C1",
+ "unicode_alternates": [],
+ "name": "open folder",
+ "shortname": ":folder_open:",
+ "category": "objects_symbols",
+ "aliases": [":open_folder:"],
+ "aliases_ascii": [],
+ "keywords": ["documents", "load"]
+ },
+ "football": {
+ "unicode": "1F3C8",
+ "unicode_alternates": [],
+ "name": "american football",
+ "shortname": ":football:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["NFL", "balls", "sports", "football", "ball", "sport", "america", "american"],
+ "moji": "🏈"
+ },
+ "footprints": {
+ "unicode": "1F463",
+ "unicode_alternates": [],
+ "name": "footprints",
+ "shortname": ":footprints:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["feet"],
+ "moji": "👣"
+ },
+ "fork_and_knife": {
+ "unicode": "1F374",
+ "unicode_alternates": [],
+ "name": "fork and knife",
+ "shortname": ":fork_and_knife:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cutlery", "kitchen", "fork", "knife", "restaurant", "meal", "food", "eat"],
+ "moji": "🍴"
+ },
+ "fork_knife_plate": {
+ "unicode": "1F37D",
+ "unicode_alternates": [],
+ "name": "fork and knife with plate",
+ "shortname": ":fork_knife_plate:",
+ "category": "travel_places",
+ "aliases": [":fork_and_knife_with_plate:"],
+ "aliases_ascii": [],
+ "keywords": ["meal", "food", "breakfast", "lunch", "dinner", "utensils", "setting"]
+ },
+ "fountain": {
+ "unicode": "26F2",
+ "unicode_alternates": ["26F2-FE0F"],
+ "name": "fountain",
+ "shortname": ":fountain:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo"],
+ "moji": "⛲"
+ },
+ "four": {
+ "moji": "4️⃣",
+ "unicode": "0034-20E3",
+ "unicode_alternates": ["0034-FE0F-20E3"],
+ "name": "digit four",
+ "shortname": ":four:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["4", "blue-square", "numbers"]
+ },
+ "four_leaf_clover": {
+ "unicode": "1F340",
+ "unicode_alternates": [],
+ "name": "four leaf clover",
+ "shortname": ":four_leaf_clover:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["lucky", "nature", "plant", "vegetable", "clover", "four", "leaf", "luck", "irish", "saint", "patrick", "green"],
+ "moji": "🍀"
+ },
+ "frame_photo": {
+ "unicode": "1F5BC",
+ "unicode_alternates": [],
+ "name": "frame with picture",
+ "shortname": ":frame_photo:",
+ "category": "objects_symbols",
+ "aliases": [":frame_with_picture:"],
+ "aliases_ascii": [],
+ "keywords": ["photo"]
+ },
+ "frame_tiles": {
+ "unicode": "1F5BD",
+ "unicode_alternates": [],
+ "name": "frame with tiles",
+ "shortname": ":frame_tiles:",
+ "category": "objects_symbols",
+ "aliases": [":frame_with_tiles:"],
+ "aliases_ascii": [],
+ "keywords": ["photo", "painting"]
+ },
+ "frame_x": {
+ "unicode": "1F5BE",
+ "unicode_alternates": [],
+ "name": "frame with an x",
+ "shortname": ":frame_x:",
+ "category": "objects_symbols",
+ "aliases": [":frame_with_an_x:"],
+ "aliases_ascii": [],
+ "keywords": ["photo", "painting"]
+ },
+ "free": {
+ "unicode": "1F193",
+ "unicode_alternates": [],
+ "name": "squared free",
+ "shortname": ":free:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "words"],
+ "moji": "🆓"
+ },
+ "fried_shrimp": {
+ "unicode": "1F364",
+ "unicode_alternates": [],
+ "name": "fried shrimp",
+ "shortname": ":fried_shrimp:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "food", "shrimp", "fried", "seafood", "small", "fish"],
+ "moji": "🍤"
+ },
+ "fries": {
+ "unicode": "1F35F",
+ "unicode_alternates": [],
+ "name": "french fries",
+ "shortname": ":fries:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chips", "food", "fries", "french", "potato", "fry", "russet", "idaho"],
+ "moji": "🍟"
+ },
+ "frog": {
+ "unicode": "1F438",
+ "unicode_alternates": [],
+ "name": "frog face",
+ "shortname": ":frog:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐸"
+ },
+ "frowning": {
+ "unicode": "1F626",
+ "unicode_alternates": [],
+ "name": "frowning face with open mouth",
+ "shortname": ":frowning:",
+ "category": "emoticons",
+ "aliases": [":anguished:"],
+ "aliases_ascii": [],
+ "keywords": ["aw", "face", "frown", "sad", "pout", "sulk", "glower"],
+ "moji": "😦"
+ },
+ "fuelpump": {
+ "unicode": "26FD",
+ "unicode_alternates": ["26FD-FE0F"],
+ "name": "fuel pump",
+ "shortname": ":fuelpump:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["gas station", "petroleum"],
+ "moji": "⛽"
+ },
+ "full_moon": {
+ "unicode": "1F315",
+ "unicode_alternates": [],
+ "name": "full moon symbol",
+ "shortname": ":full_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "yellow", "moon", "full", "sky", "night", "cheese", "phase", "monster", "spooky", "werewolves", "twilight"],
+ "moji": "🌕"
+ },
+ "full_moon_with_face": {
+ "unicode": "1F31D",
+ "unicode_alternates": [],
+ "name": "full moon with face",
+ "shortname": ":full_moon_with_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "moon", "full", "anthropomorphic", "face", "sky", "night", "cheese", "phase", "spooky", "werewolves", "monsters"],
+ "moji": "🌝"
+ },
+ "game_die": {
+ "unicode": "1F3B2",
+ "unicode_alternates": [],
+ "name": "game die",
+ "shortname": ":game_die:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dice", "game", "die", "dice", "craps", "gamble", "play"],
+ "moji": "🎲"
+ },
+ "gem": {
+ "unicode": "1F48E",
+ "unicode_alternates": [],
+ "name": "gem stone",
+ "shortname": ":gem:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue", "ruby"],
+ "moji": "💎"
+ },
+ "gemini": {
+ "unicode": "264A",
+ "unicode_alternates": ["264A-FE0F"],
+ "name": "gemini",
+ "shortname": ":gemini:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["gemini", "twins", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "moji": "♊"
+ },
+ "ghost": {
+ "unicode": "1F47B",
+ "unicode_alternates": [],
+ "name": "ghost",
+ "shortname": ":ghost:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["halloween"],
+ "moji": "👻"
+ },
+ "gift": {
+ "unicode": "1F381",
+ "unicode_alternates": [],
+ "name": "wrapped present",
+ "shortname": ":gift:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["birthday", "christmas", "present", "xmas", "gift", "present", "wrap", "package", "birthday", "wedding"],
+ "moji": "🎁"
+ },
+ "gift_heart": {
+ "unicode": "1F49D",
+ "unicode_alternates": [],
+ "name": "heart with ribbon",
+ "shortname": ":gift_heart:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["love", "valentines"],
+ "moji": "💝"
+ },
+ "girl": {
+ "unicode": "1F467",
+ "unicode_alternates": [],
+ "name": "girl",
+ "shortname": ":girl:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "woman"],
+ "moji": "👧"
+ },
+ "girls_symbol": {
+ "unicode": "1F6CA",
+ "unicode_alternates": [],
+ "name": "girls symbol",
+ "shortname": ":girls_symbol:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "child"]
+ },
+ "globe_with_meridians": {
+ "unicode": "1F310",
+ "unicode_alternates": [],
+ "name": "globe with meridians",
+ "shortname": ":globe_with_meridians:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["earth", "international", "world", "earth", "meridian", "globe", "space", "planet", "home"],
+ "moji": "🌐"
+ },
+ "goat": {
+ "unicode": "1F410",
+ "unicode_alternates": [],
+ "name": "goat",
+ "shortname": ":goat:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "goat", "sheep", "kid", "billy", "livestock"],
+ "moji": "🐐"
+ },
+ "golf": {
+ "unicode": "26F3",
+ "unicode_alternates": ["26F3-FE0F"],
+ "name": "flag in hole",
+ "shortname": ":golf:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["business", "sports"],
+ "moji": "⛳"
+ },
+ "golfer": {
+ "unicode": "1F3CC",
+ "unicode_alternates": [],
+ "name": "golfer",
+ "shortname": ":golfer:",
+ "category": "activity",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sport", "par", "birdie", "eagle", "mulligan"]
+ },
+ "grapes": {
+ "unicode": "1F347",
+ "unicode_alternates": [],
+ "name": "grapes",
+ "shortname": ":grapes:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "grapes", "wine", "vinegar", "fruit", "cluster", "vine"],
+ "moji": "🍇"
+ },
+ "green_apple": {
+ "unicode": "1F34F",
+ "unicode_alternates": [],
+ "name": "green apple",
+ "shortname": ":green_apple:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fruit", "nature", "apple", "fruit", "green", "pie", "granny", "smith", "core"],
+ "moji": "🍏"
+ },
+ "green_book": {
+ "unicode": "1F4D7",
+ "unicode_alternates": [],
+ "name": "green book",
+ "shortname": ":green_book:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["knowledge", "library", "read"],
+ "moji": "📗"
+ },
+ "green_heart": {
+ "unicode": "1F49A",
+ "unicode_alternates": [],
+ "name": "green heart",
+ "shortname": ":green_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines", "green", "heart", "love", "nature", "rebirth", "reborn", "jealous", "clingy", "envious", "possessive"],
+ "moji": "💚"
+ },
+ "grey_exclamation": {
+ "unicode": "2755",
+ "unicode_alternates": [],
+ "name": "white exclamation mark ornament",
+ "shortname": ":grey_exclamation:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["surprise"],
+ "moji": "❕"
+ },
+ "grey_question": {
+ "unicode": "2754",
+ "unicode_alternates": [],
+ "name": "white question mark ornament",
+ "shortname": ":grey_question:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["doubts"],
+ "moji": "❔"
+ },
+ "grimacing": {
+ "unicode": "1F62C",
+ "unicode_alternates": [],
+ "name": "grimacing face",
+ "shortname": ":grimacing:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "grimace", "teeth", "grimace", "disapprove", "pain"],
+ "moji": "😬"
+ },
+ "grin": {
+ "unicode": "1F601",
+ "unicode_alternates": [],
+ "name": "grinning face with smiling eyes",
+ "shortname": ":grin:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"],
+ "moji": "😁"
+ },
+ "grinning": {
+ "unicode": "1F600",
+ "unicode_alternates": [],
+ "name": "grinning face",
+ "shortname": ":grinning:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "happy", "joy", "smile", "grin", "grinning", "smiling", "smile", "smiley"],
+ "moji": "🕧"
+ },
+ "guardsman": {
+ "unicode": "1F482",
+ "unicode_alternates": [],
+ "name": "guardsman",
+ "shortname": ":guardsman:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["british", "gb", "male", "man", "uk", "guardsman", "guard", "bearskin", "hat", "british", "queen", "ceremonial", "military"],
+ "moji": "💂"
+ },
+ "guitar": {
+ "unicode": "1F3B8",
+ "unicode_alternates": [],
+ "name": "guitar",
+ "shortname": ":guitar:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["instrument", "music", "guitar", "string", "music", "instrument", "jam", "rock", "acoustic", "electric"],
+ "moji": "🎸"
+ },
+ "gun": {
+ "unicode": "1F52B",
+ "unicode_alternates": [],
+ "name": "pistol",
+ "shortname": ":gun:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["violence", "weapon"],
+ "moji": "🔫"
+ },
+ "haircut": {
+ "unicode": "1F487",
+ "unicode_alternates": [],
+ "name": "haircut",
+ "shortname": ":haircut:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman"],
+ "moji": "💇"
+ },
+ "hamburger": {
+ "unicode": "1F354",
+ "unicode_alternates": [],
+ "name": "hamburger",
+ "shortname": ":hamburger:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "meat", "hamburger", "burger", "meat", "cow", "beef"],
+ "moji": "🍔"
+ },
+ "hammer": {
+ "unicode": "1F528",
+ "unicode_alternates": [],
+ "name": "hammer",
+ "shortname": ":hammer:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["done", "judge", "law", "ruling", "tools", "verdict"],
+ "moji": "🔨"
+ },
+ "hamster": {
+ "unicode": "1F439",
+ "unicode_alternates": [],
+ "name": "hamster face",
+ "shortname": ":hamster:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐹"
+ },
+ "hand_splayed": {
+ "unicode": "1F590",
+ "unicode_alternates": [],
+ "name": "raised hand with fingers splayed",
+ "shortname": ":hand_splayed:",
+ "category": "people",
+ "aliases": [":raised_hand_with_fingers_splayed:"],
+ "aliases_ascii": [],
+ "keywords": ["hi", "five", "stop", "halt"]
+ },
+ "hand_splayed_reverse": {
+ "unicode": "1F591",
+ "unicode_alternates": [],
+ "name": "reversed raised hand with fingers splayed",
+ "shortname": ":hand_splayed_reverse:",
+ "category": "people",
+ "aliases": [":reversed_raised_hand_with_fingers_splayed:"],
+ "aliases_ascii": [],
+ "keywords": ["hi", "five", "stop", "halt"]
+ },
+ "hand_victory": {
+ "unicode": "1F594",
+ "unicode_alternates": [],
+ "name": "reversed victory hand",
+ "shortname": ":hand_victory:",
+ "category": "people",
+ "aliases": [":reversed_victory_hand:"],
+ "aliases_ascii": [],
+ "keywords": ["fu"]
+ },
+ "handbag": {
+ "unicode": "1F45C",
+ "unicode_alternates": [],
+ "name": "handbag",
+ "shortname": ":handbag:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accessories", "accessory", "bag", "fashion"],
+ "moji": "👜"
+ },
+ "hard_disk": {
+ "unicode": "1F5B4",
+ "unicode_alternates": [],
+ "name": "hard disk",
+ "shortname": ":hard_disk:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["save", "technology", "storage", "information", "computer", "drive", "megabyte", "gigabyte", "hd"]
+ },
+ "hash": {
+ "moji": "#⃣",
+ "unicode": "0023-20E3",
+ "unicode_alternates": ["0023-FE0F-20E3"],
+ "name": "number sign",
+ "shortname": ":hash:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["symbol"]
+ },
+ "hatched_chick": {
+ "unicode": "1F425",
+ "unicode_alternates": [],
+ "name": "front-facing baby chick",
+ "shortname": ":hatched_chick:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["baby", "chicken", "chick", "baby", "bird", "chicken", "young", "woman", "cute"],
+ "moji": "🐥"
+ },
+ "hatching_chick": {
+ "unicode": "1F423",
+ "unicode_alternates": [],
+ "name": "hatching chick",
+ "shortname": ":hatching_chick:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["born", "chicken", "egg", "chick", "egg", "baby", "bird", "chicken", "young", "woman", "cute"],
+ "moji": "🐣"
+ },
+ "headphones": {
+ "unicode": "1F3A7",
+ "unicode_alternates": [],
+ "name": "headphone",
+ "shortname": ":headphones:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["gadgets", "music", "score", "headphone", "sound", "music", "ears", "beats", "buds", "audio", "listen"],
+ "moji": "🎧"
+ },
+ "hear_no_evil": {
+ "unicode": "1F649",
+ "unicode_alternates": [],
+ "name": "hear-no-evil monkey",
+ "shortname": ":hear_no_evil:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "monkey", "monkey", "ears", "hear", "sound", "kikazaru"],
+ "moji": "🙉"
+ },
+ "heart": {
+ "moji": "❤",
+ "unicode": "2764",
+ "unicode_alternates": ["2764-FE0F"],
+ "name": "heavy black heart",
+ "shortname": ":heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["<3"],
+ "keywords": ["like", "love", "red", "pink", "black", "heart", "love", "passion", "romance", "intense", "desire", "death", "evil", "cold", "valentines"]
+ },
+ "heart_decoration": {
+ "unicode": "1F49F",
+ "unicode_alternates": [],
+ "name": "heart decoration",
+ "shortname": ":heart_decoration:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["like", "love", "purple-square"],
+ "moji": "💟"
+ },
+ "heart_eyes": {
+ "unicode": "1F60D",
+ "unicode_alternates": [],
+ "name": "smiling face with heart-shaped eyes",
+ "shortname": ":heart_eyes:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "crush", "face", "infatuation", "like", "love", "valentines", "smiling", "heart", "lovestruck", "love", "flirt", "smile", "heart-shaped"],
+ "moji": "😍"
+ },
+ "heart_eyes_cat": {
+ "unicode": "1F63B",
+ "unicode_alternates": [],
+ "name": "smiling cat face with heart-shaped eyes",
+ "shortname": ":heart_eyes_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "animal", "cats", "like", "love", "valentines", "lovestruck", "love", "heart"],
+ "moji": "😻"
+ },
+ "heart_tip": {
+ "unicode": "1F394",
+ "unicode_alternates": [],
+ "name": "heart with tip on the left",
+ "shortname": ":heart_tip:",
+ "category": "celebration",
+ "aliases": [":heart_with_tip_on_the_left:"],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines"]
+ },
+ "heartbeat": {
+ "unicode": "1F493",
+ "unicode_alternates": [],
+ "name": "beating heart",
+ "shortname": ":heartbeat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines"],
+ "moji": "💓"
+ },
+ "heartpulse": {
+ "unicode": "1F497",
+ "unicode_alternates": [],
+ "name": "growing heart",
+ "shortname": ":heartpulse:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines"],
+ "moji": "💗"
+ },
+ "hearts": {
+ "unicode": "2665",
+ "unicode_alternates": ["2665-FE0F"],
+ "name": "black heart suit",
+ "shortname": ":hearts:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cards", "poker"],
+ "moji": "♥"
+ },
+ "heavy_check_mark": {
+ "unicode": "2714",
+ "unicode_alternates": ["2714-FE0F"],
+ "name": "heavy check mark",
+ "shortname": ":heavy_check_mark:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nike", "ok"],
+ "moji": "✔"
+ },
+ "heavy_division_sign": {
+ "unicode": "2797",
+ "unicode_alternates": [],
+ "name": "heavy division sign",
+ "shortname": ":heavy_division_sign:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["calculation", "divide", "math"],
+ "moji": "➗"
+ },
+ "heavy_dollar_sign": {
+ "unicode": "1F4B2",
+ "unicode_alternates": [],
+ "name": "heavy dollar sign",
+ "shortname": ":heavy_dollar_sign:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["currency", "money", "payment", "dollar", "currency", "money", "cash", "sale", "purchase", "value"],
+ "moji": "💲"
+ },
+ "heavy_minus_sign": {
+ "unicode": "2796",
+ "unicode_alternates": [],
+ "name": "heavy minus sign",
+ "shortname": ":heavy_minus_sign:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["calculation", "math"],
+ "moji": "➖"
+ },
+ "heavy_multiplication_x": {
+ "unicode": "2716",
+ "unicode_alternates": ["2716-FE0F"],
+ "name": "heavy multiplication x",
+ "shortname": ":heavy_multiplication_x:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["calculation", "math"],
+ "moji": "✖"
+ },
+ "heavy_plus_sign": {
+ "unicode": "2795",
+ "unicode_alternates": [],
+ "name": "heavy plus sign",
+ "shortname": ":heavy_plus_sign:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["calculation", "math"],
+ "moji": "➕"
+ },
+ "helicopter": {
+ "unicode": "1F681",
+ "unicode_alternates": [],
+ "name": "helicopter",
+ "shortname": ":helicopter:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "helicopter", "helo", "gyro", "gyrocopter"],
+ "moji": "🚁"
+ },
+ "herb": {
+ "unicode": "1F33F",
+ "unicode_alternates": [],
+ "name": "herb",
+ "shortname": ":herb:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["grass", "lawn", "medicine", "plant", "vegetable", "weed", "herb", "spice", "plant", "cook", "cooking"],
+ "moji": "🌿"
+ },
+ "hibiscus": {
+ "unicode": "1F33A",
+ "unicode_alternates": [],
+ "name": "hibiscus",
+ "shortname": ":hibiscus:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flowers", "plant", "vegetable", "hibiscus", "flower", "warm"],
+ "moji": "🌺"
+ },
+ "high_brightness": {
+ "unicode": "1F506",
+ "unicode_alternates": [],
+ "name": "high brightness symbol",
+ "shortname": ":high_brightness:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["light", "summer", "sun"],
+ "moji": "🔆"
+ },
+ "high_heel": {
+ "unicode": "1F460",
+ "unicode_alternates": [],
+ "name": "high-heeled shoe",
+ "shortname": ":high_heel:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "female", "shoes"],
+ "moji": "👠"
+ },
+ "hole": {
+ "unicode": "1F573",
+ "unicode_alternates": [],
+ "name": "hole",
+ "shortname": ":hole:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["pit", "well"]
+ },
+ "homes": {
+ "unicode": "1F3D8",
+ "unicode_alternates": [],
+ "name": "house buildings",
+ "shortname": ":homes:",
+ "category": "travel_places",
+ "aliases": [":house_buildings:"],
+ "aliases_ascii": [],
+ "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"]
+ },
+ "honey_pot": {
+ "unicode": "1F36F",
+ "unicode_alternates": [],
+ "name": "honey pot",
+ "shortname": ":honey_pot:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bees", "sweet", "honey", "pot", "bees", "pooh", "bear"],
+ "moji": "🍯"
+ },
+ "horse": {
+ "unicode": "1F434",
+ "unicode_alternates": [],
+ "name": "horse face",
+ "shortname": ":horse:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "brown"],
+ "moji": "🐴"
+ },
+ "horse_racing": {
+ "unicode": "1F3C7",
+ "unicode_alternates": [],
+ "name": "horse racing",
+ "shortname": ":horse_racing:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "betting", "competition", "horse", "race", "racing", "jockey", "triple crown"],
+ "moji": "🏇"
+ },
+ "hospital": {
+ "unicode": "1F3E5",
+ "unicode_alternates": [],
+ "name": "hospital",
+ "shortname": ":hospital:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "doctor", "health", "surgery"],
+ "moji": "🏥"
+ },
+ "hot_pepper": {
+ "unicode": "1F336",
+ "unicode_alternates": [],
+ "name": "hot pepper",
+ "shortname": ":hot_pepper:",
+ "category": "food_drink",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "nature", "spicy", "chili", "cayenne", "habanero", "jalapeno"]
+ },
+ "hotel": {
+ "unicode": "1F3E8",
+ "unicode_alternates": [],
+ "name": "hotel",
+ "shortname": ":hotel:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accomodation", "building", "checkin", "whotel", "hotel", "motel", "holiday inn", "hospital"],
+ "moji": "🏨"
+ },
+ "hotsprings": {
+ "unicode": "2668",
+ "unicode_alternates": ["2668-FE0F"],
+ "name": "hot springs",
+ "shortname": ":hotsprings:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bath", "relax", "warm"],
+ "moji": "♨"
+ },
+ "hourglass": {
+ "unicode": "231B",
+ "unicode_alternates": ["231B-FE0F"],
+ "name": "hourglass",
+ "shortname": ":hourglass:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clock", "oldschool", "time"],
+ "moji": "⌛"
+ },
+ "hourglass_flowing_sand": {
+ "unicode": "23F3",
+ "unicode_alternates": [],
+ "name": "hourglass with flowing sand",
+ "shortname": ":hourglass_flowing_sand:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["countdown", "oldschool", "time"],
+ "moji": "⏳"
+ },
+ "house": {
+ "unicode": "1F3E0",
+ "unicode_alternates": [],
+ "name": "house building",
+ "shortname": ":house:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "home", "house", "home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman"],
+ "moji": "🏠"
+ },
+ "house_abandoned": {
+ "unicode": "1F3DA",
+ "unicode_alternates": [],
+ "name": "derelict house building",
+ "shortname": ":house_abandoned:",
+ "category": "travel_places",
+ "aliases": [":derelict_house_building:"],
+ "aliases_ascii": [],
+ "keywords": ["home", "residence", "dwelling", "mansion", "bungalow", "ranch", "craftsman", "boarded", "abandoned", "vacant", "run down", "shoddy"]
+ },
+ "house_with_garden": {
+ "unicode": "1F3E1",
+ "unicode_alternates": [],
+ "name": "house with garden",
+ "shortname": ":house_with_garden:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["home", "nature", "plant"],
+ "moji": "🏡"
+ },
+ "hushed": {
+ "unicode": "1F62F",
+ "unicode_alternates": [],
+ "name": "hushed face",
+ "shortname": ":hushed:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "woo", "quiet", "hush", "whisper", "silent"],
+ "moji": "😯"
+ },
+ "ice_cream": {
+ "unicode": "1F368",
+ "unicode_alternates": [],
+ "name": "ice cream",
+ "shortname": ":ice_cream:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "waffle"],
+ "moji": "🍨"
+ },
+ "icecream": {
+ "unicode": "1F366",
+ "unicode_alternates": [],
+ "name": "soft ice cream",
+ "shortname": ":icecream:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "food", "hot", "icecream", "ice", "cream", "dairy", "dessert", "cold", "soft", "serve", "cone", "yogurt"],
+ "moji": "🍦"
+ },
+ "ideograph_advantage": {
+ "unicode": "1F250",
+ "unicode_alternates": [],
+ "name": "circled ideograph advantage",
+ "shortname": ":ideograph_advantage:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "get", "kanji", "obtain"],
+ "moji": "🉐"
+ },
+ "imp": {
+ "unicode": "1F47F",
+ "unicode_alternates": [],
+ "name": "imp",
+ "shortname": ":imp:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["angry", "devil", "evil", "horns", "cute", "devil"],
+ "moji": "👿"
+ },
+ "inbox_tray": {
+ "unicode": "1F4E5",
+ "unicode_alternates": [],
+ "name": "inbox tray",
+ "shortname": ":inbox_tray:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents", "email"],
+ "moji": "📥"
+ },
+ "incoming_envelope": {
+ "unicode": "1F4E8",
+ "unicode_alternates": [],
+ "name": "incoming envelope",
+ "shortname": ":incoming_envelope:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["email", "inbox"],
+ "moji": "📨"
+ },
+ "info": {
+ "unicode": "1F6C8",
+ "unicode_alternates": [],
+ "name": "circled information source",
+ "shortname": ":info:",
+ "category": "objects_symbols",
+ "aliases": [":circled_information_source:"],
+ "aliases_ascii": [],
+ "keywords": ["icon"]
+ },
+ "information_desk_person": {
+ "unicode": "1F481",
+ "unicode_alternates": [],
+ "name": "information desk person",
+ "shortname": ":information_desk_person:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "human", "woman", "information", "help", "question", "answer", "sassy", "unimpressed", "attitude", "snarky"],
+ "moji": "💁"
+ },
+ "information_source": {
+ "unicode": "2139",
+ "unicode_alternates": ["2139-FE0F"],
+ "name": "information source",
+ "shortname": ":information_source:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "blue-square", "letter"],
+ "moji": "ℹ"
+ },
+ "innocent": {
+ "unicode": "1F607",
+ "unicode_alternates": [],
+ "name": "smiling face with halo",
+ "shortname": ":innocent:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["O:-)", "0:-3", "0:3", "0:-)", "0:)", "0;^)", "O:-)", "O:)", "O;-)", "O=)", "0;-)", "O:-3", "O:3"],
+ "keywords": ["angel", "face", "halo", "halo", "angel", "innocent", "ring", "circle", "heaven"],
+ "moji": "😇"
+ },
+ "interrobang": {
+ "unicode": "2049",
+ "unicode_alternates": ["2049-FE0F"],
+ "name": "exclamation question mark",
+ "shortname": ":interrobang:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["punctuation", "surprise", "wat"],
+ "moji": "⁉"
+ },
+ "iphone": {
+ "unicode": "1F4F1",
+ "unicode_alternates": [],
+ "name": "mobile phone",
+ "shortname": ":iphone:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["apple", "dial", "gadgets", "technology"],
+ "moji": "📱"
+ },
+ "island": {
+ "unicode": "1F3DD",
+ "unicode_alternates": [],
+ "name": "desert island",
+ "shortname": ":island:",
+ "category": "travel_places",
+ "aliases": [":desert_island:"],
+ "aliases_ascii": [],
+ "keywords": ["land", "solitude", "alone"]
+ },
+ "izakaya_lantern": {
+ "unicode": "1F3EE",
+ "unicode_alternates": [],
+ "name": "izakaya lantern",
+ "shortname": ":izakaya_lantern:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["light", "izakaya", "lantern", "stay", "drink", "alcohol", "bar", "sake", "restaurant"],
+ "moji": "🏮"
+ },
+ "jack_o_lantern": {
+ "unicode": "1F383",
+ "unicode_alternates": [],
+ "name": "jack-o-lantern",
+ "shortname": ":jack_o_lantern:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["halloween", "jack-o-lantern", "pumpkin", "halloween", "holiday", "carve", "autumn", "fall", "october", "saints", "costume", "spooky", "horror", "scary", "scared", "dead"],
+ "moji": "🎃"
+ },
+ "japan": {
+ "unicode": "1F5FE",
+ "unicode_alternates": [],
+ "name": "silhouette of japan",
+ "shortname": ":japan:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nation"],
+ "moji": "🗾"
+ },
+ "japanese_castle": {
+ "unicode": "1F3EF",
+ "unicode_alternates": [],
+ "name": "japanese castle",
+ "shortname": ":japanese_castle:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "photo", "castle", "japanese", "residence", "royalty", "fort", "fortified", "fortress"],
+ "moji": "🏯"
+ },
+ "japanese_goblin": {
+ "unicode": "1F47A",
+ "unicode_alternates": [],
+ "name": "japanese goblin",
+ "shortname": ":japanese_goblin:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["evil", "mask", "red", "japanese", "tengu", "supernatural", "avian", "demon", "goblin", "mask", "theater", "nose", "frown", "mustache", "anger", "frustration"],
+ "moji": "👺"
+ },
+ "japanese_ogre": {
+ "unicode": "1F479",
+ "unicode_alternates": [],
+ "name": "japanese ogre",
+ "shortname": ":japanese_ogre:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["monster", "japanese", "oni", "demon", "troll", "ogre", "folklore", "monster", "devil", "mask", "theater", "horns", "teeth"],
+ "moji": "👹"
+ },
+ "jeans": {
+ "unicode": "1F456",
+ "unicode_alternates": [],
+ "name": "jeans",
+ "shortname": ":jeans:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "shopping", "jeans", "pants", "blue", "denim", "levi&#039;s", "levi", "designer", "work", "skinny"],
+ "moji": "👖"
+ },
+ "jet_up": {
+ "unicode": "1F6E6",
+ "unicode_alternates": [],
+ "name": "up-pointing military airplane",
+ "shortname": ":jet_up:",
+ "category": "travel_places",
+ "aliases": [":up_pointing_military_airplane:"],
+ "aliases_ascii": [],
+ "keywords": ["jet"]
+ },
+ "joy": {
+ "unicode": "1F602",
+ "unicode_alternates": [],
+ "name": "face with tears of joy",
+ "shortname": ":joy:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":')", ":'-)"],
+ "keywords": ["cry", "face", "haha", "happy", "tears", "tears", "cry", "joy", "happy", "weep"],
+ "moji": "😂"
+ },
+ "joy_cat": {
+ "unicode": "1F639",
+ "unicode_alternates": [],
+ "name": "cat face with tears of joy",
+ "shortname": ":joy_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "haha", "happy", "tears", "happy", "tears", "cry", "joy"],
+ "moji": "😹"
+ },
+ "joystick": {
+ "unicode": "1F579",
+ "unicode_alternates": [],
+ "name": "joystick",
+ "shortname": ":joystick:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["games", "atari", "controller"]
+ },
+ "key": {
+ "unicode": "1F511",
+ "unicode_alternates": [],
+ "name": "key",
+ "shortname": ":key:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["door", "lock", "password"],
+ "moji": "🔑"
+ },
+ "key2": {
+ "unicode": "1F5DD",
+ "unicode_alternates": [],
+ "name": "old key",
+ "shortname": ":key2:",
+ "category": "objects_symbols",
+ "aliases": [":old_key:"],
+ "aliases_ascii": [],
+ "keywords": ["door", "lock", "password", "skeleton"]
+ },
+ "keyboard": {
+ "unicode": "1F5AE",
+ "unicode_alternates": [],
+ "name": "wired keyboard",
+ "shortname": ":keyboard:",
+ "category": "objects_symbols",
+ "aliases": [":wired_keyboard:"],
+ "aliases_ascii": [],
+ "keywords": ["typing", "keys", "input", "device"]
+ },
+ "keyboard_mouse": {
+ "unicode": "1F5A6",
+ "unicode_alternates": [],
+ "name": "keyboard and mouse",
+ "shortname": ":keyboard_mouse:",
+ "category": "objects_symbols",
+ "aliases": [":keyboard_and_mouse:"],
+ "aliases_ascii": [],
+ "keywords": ["computer", "input", "desktop"]
+ },
+ "keyboard_with_jacks": {
+ "unicode": "1F398",
+ "unicode_alternates": [],
+ "name": "musical keyboard with jacks",
+ "shortname": ":keyboard_with_jacks:",
+ "category": "objects_symbols",
+ "aliases": [":musical_keyboard_with_jacks:"],
+ "aliases_ascii": [],
+ "keywords": ["music", "instrument", "midi"]
+ },
+ "keycap_ten": {
+ "unicode": "1F51F",
+ "unicode_alternates": [],
+ "name": "keycap ten",
+ "shortname": ":keycap_ten:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["10", "blue-square", "numbers"],
+ "moji": "🔟"
+ },
+ "kimono": {
+ "unicode": "1F458",
+ "unicode_alternates": [],
+ "name": "kimono",
+ "shortname": ":kimono:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["dress", "fashion", "female", "japanese", "women"],
+ "moji": "👘"
+ },
+ "kiss": {
+ "unicode": "1F48B",
+ "unicode_alternates": [],
+ "name": "kiss mark",
+ "shortname": ":kiss:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "face", "like", "lips", "love", "valentines"],
+ "moji": "💋"
+ },
+ "kiss_mm": {
+ "unicode": "1F468-2764-1F48B-1F468",
+ "unicode_alternates": ["1F468-200D-2764-FE0F-200D-1F48B-200D-1F468"],
+ "name": "kiss (man,man)",
+ "shortname": ":kiss_mm:",
+ "category": "people",
+ "aliases": [":couplekiss_mm:"],
+ "aliases_ascii": [],
+ "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"]
+ },
+ "kiss_ww": {
+ "unicode": "1F469-2764-1F48B-1F469",
+ "unicode_alternates": ["1F469-200D-2764-FE0F-200D-1F48B-200D-1F469"],
+ "name": "kiss (woman,woman)",
+ "shortname": ":kiss_ww:",
+ "category": "people",
+ "aliases": [":couplekiss_ww:"],
+ "aliases_ascii": [],
+ "keywords": ["dating", "like", "love", "marriage", "valentines", "couple"]
+ },
+ "kissing": {
+ "unicode": "1F617",
+ "unicode_alternates": [],
+ "name": "kissing face",
+ "shortname": ":kissing:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["3", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "pucker", "lips", "smooch"],
+ "moji": "😗"
+ },
+ "kissing_cat": {
+ "unicode": "1F63D",
+ "unicode_alternates": [],
+ "name": "kissing cat face with closed eyes",
+ "shortname": ":kissing_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "passion", "kiss", "puckered", "heart", "love"],
+ "moji": "😽"
+ },
+ "kissing_closed_eyes": {
+ "unicode": "1F61A",
+ "unicode_alternates": [],
+ "name": "kissing face with closed eyes",
+ "shortname": ":kissing_closed_eyes:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "face", "infatuation", "like", "love", "valentines", "kissing", "kiss", "passion", "puckered", "heart", "love", "smooch"],
+ "moji": "😚"
+ },
+ "kissing_heart": {
+ "unicode": "1F618",
+ "unicode_alternates": [],
+ "name": "face throwing a kiss",
+ "shortname": ":kissing_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":*", ":-*", "=*", ":^*"],
+ "keywords": ["affection", "face", "infatuation", "kiss", "blowing kiss", "heart", "love", "lips", "like", "love", "valentines"],
+ "moji": "😘"
+ },
+ "kissing_smiling_eyes": {
+ "unicode": "1F619",
+ "unicode_alternates": [],
+ "name": "kissing face with smiling eyes",
+ "shortname": ":kissing_smiling_eyes:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "face", "infatuation", "valentines", "kissing", "kiss", "smile", "pucker", "lips", "smooch"],
+ "moji": "😙"
+ },
+ "knife": {
+ "unicode": "1F52A",
+ "unicode_alternates": [],
+ "name": "hocho",
+ "shortname": ":knife:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🔪"
+ },
+ "koala": {
+ "unicode": "1F428",
+ "unicode_alternates": [],
+ "name": "koala",
+ "shortname": ":koala:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐨"
+ },
+ "koko": {
+ "unicode": "1F201",
+ "unicode_alternates": [],
+ "name": "squared katakana koko",
+ "shortname": ":koko:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "destination", "here", "japanese", "katakana"],
+ "moji": "🈁"
+ },
+ "label": {
+ "unicode": "1F3F7",
+ "unicode_alternates": [],
+ "name": "label",
+ "shortname": ":label:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["tag"]
+ },
+ "large_blue_circle": {
+ "unicode": "1F535",
+ "unicode_alternates": [],
+ "name": "large blue circle",
+ "shortname": ":large_blue_circle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🔵"
+ },
+ "large_blue_diamond": {
+ "unicode": "1F537",
+ "unicode_alternates": [],
+ "name": "large blue diamond",
+ "shortname": ":large_blue_diamond:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔷"
+ },
+ "large_orange_diamond": {
+ "unicode": "1F536",
+ "unicode_alternates": [],
+ "name": "large orange diamond",
+ "shortname": ":large_orange_diamond:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔶"
+ },
+ "last_quarter_moon": {
+ "unicode": "1F317",
+ "unicode_alternates": [],
+ "name": "last quarter moon symbol",
+ "shortname": ":last_quarter_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "last", "quarter", "sky", "night", "cheese", "phase"],
+ "moji": "🌗"
+ },
+ "last_quarter_moon_with_face": {
+ "unicode": "1F31C",
+ "unicode_alternates": [],
+ "name": "last quarter moon with face",
+ "shortname": ":last_quarter_moon_with_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "last", "quarter", "anthropomorphic", "face", "sky", "night", "cheese", "phase"],
+ "moji": "🌜"
+ },
+ "laughing": {
+ "unicode": "1F606",
+ "unicode_alternates": [],
+ "name": "smiling face with open mouth and tightly-closed ey",
+ "shortname": ":laughing:",
+ "category": "emoticons",
+ "aliases": [":satisfied:"],
+ "aliases_ascii": [">:)", ">;)", ">:-)", ">=)"],
+ "keywords": ["happy", "joy", "lol", "smiling", "laughing", "laugh"],
+ "moji": "😆"
+ },
+ "leaves": {
+ "unicode": "1F343",
+ "unicode_alternates": [],
+ "name": "leaf fluttering in wind",
+ "shortname": ":leaves:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["grass", "lawn", "nature", "plant", "tree", "vegetable", "leaves", "leaf", "wind", "float", "fluttering"],
+ "moji": "🍃"
+ },
+ "ledger": {
+ "unicode": "1F4D2",
+ "unicode_alternates": [],
+ "name": "ledger",
+ "shortname": ":ledger:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["notes", "paper"],
+ "moji": "📒"
+ },
+ "left_luggage": {
+ "unicode": "1F6C5",
+ "unicode_alternates": [],
+ "name": "left luggage",
+ "shortname": ":left_luggage:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "travel", "bag", "baggage", "luggage", "travel"],
+ "moji": "🛅"
+ },
+ "left_receiver": {
+ "unicode": "1F57B",
+ "unicode_alternates": [],
+ "name": "left hand telephone receiver",
+ "shortname": ":left_receiver:",
+ "category": "objects_symbols",
+ "aliases": [":left_hand_telephone_receiver:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "dial", "technology"]
+ },
+ "left_right_arrow": {
+ "unicode": "2194",
+ "unicode_alternates": ["2194-FE0F"],
+ "name": "left right arrow",
+ "shortname": ":left_right_arrow:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "↔"
+ },
+ "leftwards_arrow_with_hook": {
+ "unicode": "21A9",
+ "unicode_alternates": ["21A9-FE0F"],
+ "name": "leftwards arrow with hook",
+ "shortname": ":leftwards_arrow_with_hook:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "↩"
+ },
+ "lemon": {
+ "unicode": "1F34B",
+ "unicode_alternates": [],
+ "name": "lemon",
+ "shortname": ":lemon:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fruit", "nature", "lemon", "yellow", "citrus"],
+ "moji": "🍋"
+ },
+ "leo": {
+ "unicode": "264C",
+ "unicode_alternates": ["264C-FE0F"],
+ "name": "leo",
+ "shortname": ":leo:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["leo", "lion", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "moji": "♌"
+ },
+ "leopard": {
+ "unicode": "1F406",
+ "unicode_alternates": [],
+ "name": "leopard",
+ "shortname": ":leopard:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "leopard", "cat", "spot", "spotted", "sexy"],
+ "moji": "🐆"
+ },
+ "level_slider": {
+ "unicode": "1F39A",
+ "unicode_alternates": [],
+ "name": "level slider",
+ "shortname": ":level_slider:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["controls"]
+ },
+ "levitate": {
+ "unicode": "1F574",
+ "unicode_alternates": [],
+ "name": "man in business suit levitating",
+ "shortname": ":levitate:",
+ "category": "people",
+ "aliases": [":man_in_business_suit_levitating:"],
+ "aliases_ascii": [],
+ "keywords": ["hover", "exclamation"]
+ },
+ "libra": {
+ "unicode": "264E",
+ "unicode_alternates": ["264E-FE0F"],
+ "name": "libra",
+ "shortname": ":libra:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["libra", "scales", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "moji": "♎"
+ },
+ "lifter": {
+ "unicode": "1F3CB",
+ "unicode_alternates": [],
+ "name": "weight lifter",
+ "shortname": ":lifter:",
+ "category": "activity",
+ "aliases": [":weight_lifter:"],
+ "aliases_ascii": [],
+ "keywords": ["bench", "press", "squats", "deadlift"]
+ },
+ "light_check_mark": {
+ "unicode": "1F5F8",
+ "unicode_alternates": [],
+ "name": "light check mark",
+ "shortname": ":light_check_mark:",
+ "category": "objects_symbols",
+ "aliases": [":light_mark:"],
+ "aliases_ascii": [],
+ "keywords": ["vote"]
+ },
+ "light_rail": {
+ "unicode": "1F688",
+ "unicode_alternates": [],
+ "name": "light rail",
+ "shortname": ":light_rail:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "train", "rail", "light"],
+ "moji": "🚈"
+ },
+ "link": {
+ "unicode": "1F517",
+ "unicode_alternates": [],
+ "name": "link symbol",
+ "shortname": ":link:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["rings", "url"],
+ "moji": "🔗"
+ },
+ "lips": {
+ "unicode": "1F444",
+ "unicode_alternates": [],
+ "name": "mouth",
+ "shortname": ":lips:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["kiss", "mouth"],
+ "moji": "👄"
+ },
+ "lips2": {
+ "unicode": "1F5E2",
+ "unicode_alternates": [],
+ "name": "lips",
+ "shortname": ":lips2:",
+ "category": "people",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["kiss", "mouth"]
+ },
+ "lipstick": {
+ "unicode": "1F484",
+ "unicode_alternates": [],
+ "name": "lipstick",
+ "shortname": ":lipstick:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "female", "girl"],
+ "moji": "💄"
+ },
+ "lock": {
+ "unicode": "1F512",
+ "unicode_alternates": [],
+ "name": "lock",
+ "shortname": ":lock:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["password", "security"],
+ "moji": "🔒"
+ },
+ "lock_with_ink_pen": {
+ "unicode": "1F50F",
+ "unicode_alternates": [],
+ "name": "lock with ink pen",
+ "shortname": ":lock_with_ink_pen:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["secret", "security"],
+ "moji": "🔏"
+ },
+ "lollipop": {
+ "unicode": "1F36D",
+ "unicode_alternates": [],
+ "name": "lollipop",
+ "shortname": ":lollipop:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["candy", "food", "snack", "sweet", "lollipop", "stick", "lick", "sweet", "sugar", "candy"],
+ "moji": "🍭"
+ },
+ "loop": {
+ "unicode": "27BF",
+ "unicode_alternates": [],
+ "name": "double curly loop",
+ "shortname": ":loop:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["curly"],
+ "moji": "➿"
+ },
+ "loud_sound": {
+ "unicode": "1F50A",
+ "unicode_alternates": [],
+ "name": "speaker with three sound waves",
+ "shortname": ":loud_sound:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": [],
+ "moji": "🔊"
+ },
+ "loudspeaker": {
+ "unicode": "1F4E2",
+ "unicode_alternates": [],
+ "name": "public address loudspeaker",
+ "shortname": ":loudspeaker:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sound", "volume"],
+ "moji": "📢"
+ },
+ "love_hotel": {
+ "unicode": "1F3E9",
+ "unicode_alternates": [],
+ "name": "love hotel",
+ "shortname": ":love_hotel:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "dating", "like", "love", "hotel", "love", "sex", "romance", "leisure", "adultery", "prostitution", "hospital", "birth", "happy"],
+ "moji": "🏩"
+ },
+ "love_letter": {
+ "unicode": "1F48C",
+ "unicode_alternates": [],
+ "name": "love letter",
+ "shortname": ":love_letter:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "email", "envelope", "like", "valentines", "love", "letter", "kiss", "heart"],
+ "moji": "💌"
+ },
+ "low_brightness": {
+ "unicode": "1F505",
+ "unicode_alternates": [],
+ "name": "low brightness symbol",
+ "shortname": ":low_brightness:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["summer", "sun"],
+ "moji": "🔅"
+ },
+ "m": {
+ "unicode": "24C2",
+ "unicode_alternates": ["24C2-FE0F"],
+ "name": "circled latin capital letter m",
+ "shortname": ":m:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "blue-circle", "letter"],
+ "moji": "Ⓜ"
+ },
+ "mag": {
+ "unicode": "1F50D",
+ "unicode_alternates": [],
+ "name": "left-pointing magnifying glass",
+ "shortname": ":mag:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"],
+ "moji": "🔍"
+ },
+ "mag_right": {
+ "unicode": "1F50E",
+ "unicode_alternates": [],
+ "name": "right-pointing magnifying glass",
+ "shortname": ":mag_right:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["search", "zoom", "detective", "investigator", "detail", "details"],
+ "moji": "🔎"
+ },
+ "mahjong": {
+ "unicode": "1F004",
+ "unicode_alternates": ["1F004-FE0F"],
+ "name": "mahjong tile red dragon",
+ "shortname": ":mahjong:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "game", "kanji"],
+ "moji": "🀄"
+ },
+ "mailbox": {
+ "unicode": "1F4EB",
+ "unicode_alternates": [],
+ "name": "closed mailbox with raised flag",
+ "shortname": ":mailbox:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "email", "inbox"],
+ "moji": "📫"
+ },
+ "mailbox_closed": {
+ "unicode": "1F4EA",
+ "unicode_alternates": [],
+ "name": "closed mailbox with lowered flag",
+ "shortname": ":mailbox_closed:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "email", "inbox"],
+ "moji": "📪"
+ },
+ "mailbox_with_mail": {
+ "unicode": "1F4EC",
+ "unicode_alternates": [],
+ "name": "open mailbox with raised flag",
+ "shortname": ":mailbox_with_mail:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "email", "inbox"],
+ "moji": "📬"
+ },
+ "mailbox_with_no_mail": {
+ "unicode": "1F4ED",
+ "unicode_alternates": [],
+ "name": "open mailbox with lowered flag",
+ "shortname": ":mailbox_with_no_mail:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["email", "inbox"],
+ "moji": "📭"
+ },
+ "man": {
+ "unicode": "1F468",
+ "unicode_alternates": [],
+ "name": "man",
+ "shortname": ":man:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["classy", "dad", "father", "guy", "mustashe"],
+ "moji": "👨"
+ },
+ "man_with_gua_pi_mao": {
+ "unicode": "1F472",
+ "unicode_alternates": [],
+ "name": "man with gua pi mao",
+ "shortname": ":man_with_gua_pi_mao:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["boy", "male", "skullcap", "chinese", "asian", "qing"],
+ "moji": "👲"
+ },
+ "man_with_turban": {
+ "unicode": "1F473",
+ "unicode_alternates": [],
+ "name": "man with turban",
+ "shortname": ":man_with_turban:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["male", "turban", "headdress", "headwear", "pagri", "india", "indian", "mummy", "wisdom", "peace"],
+ "moji": "👳"
+ },
+ "mans_shoe": {
+ "unicode": "1F45E",
+ "unicode_alternates": [],
+ "name": "mans shoe",
+ "shortname": ":mans_shoe:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "male"],
+ "moji": "👞"
+ },
+ "map": {
+ "unicode": "1F5FA",
+ "unicode_alternates": [],
+ "name": "world map",
+ "shortname": ":map:",
+ "category": "travel_places",
+ "aliases": [":world_map:"],
+ "aliases_ascii": [],
+ "keywords": ["atlas", "earth", "cartography"]
+ },
+ "maple_leaf": {
+ "unicode": "1F341",
+ "unicode_alternates": [],
+ "name": "maple leaf",
+ "shortname": ":maple_leaf:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["canada", "nature", "plant", "vegetable", "maple", "leaf", "syrup", "canada", "tree"],
+ "moji": "🍁"
+ },
+ "mask": {
+ "unicode": "1F637",
+ "unicode_alternates": [],
+ "name": "face with medical mask",
+ "shortname": ":mask:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "ill", "sick", "sick", "virus", "flu", "medical", "mask"],
+ "moji": "😷"
+ },
+ "massage": {
+ "unicode": "1F486",
+ "unicode_alternates": [],
+ "name": "face massage",
+ "shortname": ":massage:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman"],
+ "moji": "💆"
+ },
+ "meat_on_bone": {
+ "unicode": "1F356",
+ "unicode_alternates": [],
+ "name": "meat on bone",
+ "shortname": ":meat_on_bone:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "good", "meat", "bone", "animal", "cooked"],
+ "moji": "🍖"
+ },
+ "medal": {
+ "unicode": "1F3C5",
+ "unicode_alternates": [],
+ "name": "sports medal",
+ "shortname": ":medal:",
+ "category": "activity",
+ "aliases": [":sports_medal:"],
+ "aliases_ascii": [],
+ "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "first", "show", "reward", "achievement"]
+ },
+ "mega": {
+ "unicode": "1F4E3",
+ "unicode_alternates": [],
+ "name": "cheering megaphone",
+ "shortname": ":mega:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sound", "speaker", "volume"],
+ "moji": "📣"
+ },
+ "melon": {
+ "unicode": "1F348",
+ "unicode_alternates": [],
+ "name": "melon",
+ "shortname": ":melon:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "nature", "melon", "cantaloupe", "honeydew"],
+ "moji": "🍈"
+ },
+ "mens": {
+ "unicode": "1F6B9",
+ "unicode_alternates": [],
+ "name": "mens symbol",
+ "shortname": ":mens:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["restroom", "toilet", "wc", "men", "bathroom", "restroom", "sign", "boy", "male", "avatar"],
+ "moji": "🚹"
+ },
+ "metro": {
+ "unicode": "1F687",
+ "unicode_alternates": [],
+ "name": "metro",
+ "shortname": ":metro:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "mrt", "transportation", "tube", "underground", "metro", "subway", "underground", "train"],
+ "moji": "🚇"
+ },
+ "microphone": {
+ "unicode": "1F3A4",
+ "unicode_alternates": [],
+ "name": "microphone",
+ "shortname": ":microphone:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["PA", "music", "sound", "microphone", "mic", "audio", "sound", "voice", "karaoke"],
+ "moji": "🎤"
+ },
+ "microphone2": {
+ "unicode": "1F399",
+ "unicode_alternates": [],
+ "name": "studio microphone",
+ "shortname": ":microphone2:",
+ "category": "objects_symbols",
+ "aliases": [":studio_microphone:"],
+ "aliases_ascii": [],
+ "keywords": ["mic", "audio", "recording"]
+ },
+ "microscope": {
+ "unicode": "1F52C",
+ "unicode_alternates": [],
+ "name": "microscope",
+ "shortname": ":microscope:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["experiment", "laboratory", "zoomin"],
+ "moji": "🔬"
+ },
+ "middle_finger": {
+ "unicode": "1F595",
+ "unicode_alternates": [],
+ "name": "reversed hand with middle finger extended",
+ "shortname": ":middle_finger:",
+ "category": "people",
+ "aliases": [":reversed_hand_with_middle_finger_extended:"],
+ "aliases_ascii": [],
+ "keywords": ["fu"]
+ },
+ "military_medal": {
+ "unicode": "1F396",
+ "unicode_alternates": [],
+ "name": "military medal",
+ "shortname": ":military_medal:",
+ "category": "celebration",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["honor", "acknowledgment", "purple heart", "heroism", "veteran"]
+ },
+ "milky_way": {
+ "unicode": "1F30C",
+ "unicode_alternates": [],
+ "name": "milky way",
+ "shortname": ":milky_way:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo", "space", "milky", "galaxy", "star", "stars", "planets", "space", "sky"],
+ "moji": "🌌"
+ },
+ "minibus": {
+ "unicode": "1F690",
+ "unicode_alternates": [],
+ "name": "minibus",
+ "shortname": ":minibus:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["car", "transportation", "vehicle", "bus", "city", "transport", "transportation"],
+ "moji": "🚐"
+ },
+ "minidisc": {
+ "unicode": "1F4BD",
+ "unicode_alternates": [],
+ "name": "minidisc",
+ "shortname": ":minidisc:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["data", "disc", "disk", "record", "technology"],
+ "moji": "💽"
+ },
+ "mobile_phone_off": {
+ "unicode": "1F4F4",
+ "unicode_alternates": [],
+ "name": "mobile phone off",
+ "shortname": ":mobile_phone_off:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mute"],
+ "moji": "📴"
+ },
+ "money_with_wings": {
+ "unicode": "1F4B8",
+ "unicode_alternates": [],
+ "name": "money with wings",
+ "shortname": ":money_with_wings:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bills", "dollar", "payment", "money", "wings", "easy", "spend", "work", "lost", "blown", "burned", "gift", "cash", "dollar"],
+ "moji": "💸"
+ },
+ "moneybag": {
+ "unicode": "1F4B0",
+ "unicode_alternates": [],
+ "name": "money bag",
+ "shortname": ":moneybag:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["coins", "dollar", "payment"],
+ "moji": "💰"
+ },
+ "monkey": {
+ "unicode": "1F412",
+ "unicode_alternates": [],
+ "name": "monkey",
+ "shortname": ":monkey:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "monkey", "primate", "banana", "silly"],
+ "moji": "🐒"
+ },
+ "monkey_face": {
+ "unicode": "1F435",
+ "unicode_alternates": [],
+ "name": "monkey face",
+ "shortname": ":monkey_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐵"
+ },
+ "monorail": {
+ "unicode": "1F69D",
+ "unicode_alternates": [],
+ "name": "monorail",
+ "shortname": ":monorail:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "train", "mono", "rail", "transport"],
+ "moji": "🚝"
+ },
+ "mood_bubble": {
+ "unicode": "1F5F0",
+ "unicode_alternates": [],
+ "name": "mood bubble",
+ "shortname": ":mood_bubble:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "conversation", "communication", "comic", "feeling"]
+ },
+ "mood_bubble_lightning": {
+ "unicode": "1F5F1",
+ "unicode_alternates": [],
+ "name": "lightning mood bubble",
+ "shortname": ":mood_bubble_lightning:",
+ "category": "objects_symbols",
+ "aliases": [":lightning_mood_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "conversation", "communication", "comic", "feeling"]
+ },
+ "mood_lightning": {
+ "unicode": "1F5F2",
+ "unicode_alternates": [],
+ "name": "lightning mood",
+ "shortname": ":mood_lightning:",
+ "category": "objects_symbols",
+ "aliases": [":lightning_mood:"],
+ "aliases_ascii": [],
+ "keywords": ["zap", "electric", "current"]
+ },
+ "mortar_board": {
+ "unicode": "1F393",
+ "unicode_alternates": [],
+ "name": "graduation cap",
+ "shortname": ":mortar_board:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cap", "college", "degree", "graduation", "hat", "school", "university", "graduation", "cap", "mortarboard", "academic", "education", "ceremony", "square", "tassel"],
+ "moji": "🎓"
+ },
+ "motorboat": {
+ "unicode": "1F6E5",
+ "unicode_alternates": [],
+ "name": "motorboat",
+ "shortname": ":motorboat:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "boat", "speedboat", "powerboat"]
+ },
+ "motorcycle": {
+ "unicode": "1F3CD",
+ "unicode_alternates": [],
+ "name": "racing motorcycle",
+ "shortname": ":motorcycle:",
+ "category": "activity",
+ "aliases": [":racing_motorcycle:"],
+ "aliases_ascii": [],
+ "keywords": ["bike", "speed"]
+ },
+ "motorway": {
+ "unicode": "1F6E3",
+ "unicode_alternates": [],
+ "name": "motorway",
+ "shortname": ":motorway:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["road", "highway", "freeway", "traffic", "travel"]
+ },
+ "mount_fuji": {
+ "unicode": "1F5FB",
+ "unicode_alternates": [],
+ "name": "mount fuji",
+ "shortname": ":mount_fuji:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["japan", "mountain", "nature", "photo"],
+ "moji": "🗻"
+ },
+ "mountain_bicyclist": {
+ "unicode": "1F6B5",
+ "unicode_alternates": [],
+ "name": "mountain bicyclist",
+ "shortname": ":mountain_bicyclist:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["human", "sports", "transportation", "bicyclist", "mountain", "bike", "pedal", "bicycle", "transportation"],
+ "moji": "🚵"
+ },
+ "mountain_cableway": {
+ "unicode": "1F6A0",
+ "unicode_alternates": [],
+ "name": "mountain cableway",
+ "shortname": ":mountain_cableway:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "mountain", "cable", "rail", "train", "railway"],
+ "moji": "🚠"
+ },
+ "mountain_railway": {
+ "unicode": "1F69E",
+ "unicode_alternates": [],
+ "name": "mountain railway",
+ "shortname": ":mountain_railway:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "mountain", "railway", "rail", "train", "transport"],
+ "moji": "🚞"
+ },
+ "mountain_snow": {
+ "unicode": "1F3D4",
+ "unicode_alternates": [],
+ "name": "snow capped mountain",
+ "shortname": ":mountain_snow:",
+ "category": "travel_places",
+ "aliases": [":snow_capped_mountain:"],
+ "aliases_ascii": [],
+ "keywords": ["cold", "elevation", "hiking", "peak"]
+ },
+ "mouse": {
+ "unicode": "1F42D",
+ "unicode_alternates": [],
+ "name": "mouse face",
+ "shortname": ":mouse:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐭"
+ },
+ "mouse2": {
+ "unicode": "1F401",
+ "unicode_alternates": [],
+ "name": "mouse",
+ "shortname": ":mouse2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "mouse", "mice", "rodent"],
+ "moji": "🐁"
+ },
+ "mouse_one": {
+ "unicode": "1F5AF",
+ "unicode_alternates": [],
+ "name": "one button mouse",
+ "shortname": ":mouse_one:",
+ "category": "objects_symbols",
+ "aliases": [":one_button_mouse:"],
+ "aliases_ascii": [],
+ "keywords": ["computer", "input", "device"]
+ },
+ "movie_camera": {
+ "unicode": "1F3A5",
+ "unicode_alternates": [],
+ "name": "movie camera",
+ "shortname": ":movie_camera:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["film", "record", "movie", "camera", "camcorder", "video", "motion", "picture"],
+ "moji": "🎥"
+ },
+ "moyai": {
+ "unicode": "1F5FF",
+ "unicode_alternates": [],
+ "name": "moyai",
+ "shortname": ":moyai:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["island", "stone"],
+ "moji": "🗿"
+ },
+ "muscle": {
+ "unicode": "1F4AA",
+ "unicode_alternates": [],
+ "name": "flexed biceps",
+ "shortname": ":muscle:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arm", "flex", "hand", "strong", "muscle", "bicep"],
+ "moji": "💪"
+ },
+ "mushroom": {
+ "unicode": "1F344",
+ "unicode_alternates": [],
+ "name": "mushroom",
+ "shortname": ":mushroom:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["plant", "vegetable", "mushroom", "fungi", "food", "fungus"],
+ "moji": "🍄"
+ },
+ "musical_keyboard": {
+ "unicode": "1F3B9",
+ "unicode_alternates": [],
+ "name": "musical keyboard",
+ "shortname": ":musical_keyboard:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["instrument", "piano", "music", "keyboard", "piano", "organ", "instrument", "electric"],
+ "moji": "🎹"
+ },
+ "musical_note": {
+ "unicode": "1F3B5",
+ "unicode_alternates": [],
+ "name": "musical note",
+ "shortname": ":musical_note:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["score", "musical", "music", "note", "music", "sound"],
+ "moji": "🎵"
+ },
+ "musical_score": {
+ "unicode": "1F3BC",
+ "unicode_alternates": [],
+ "name": "musical score",
+ "shortname": ":musical_score:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["clef", "treble", "music", "musical", "score", "clef", "g-clef", "stave", "staff"],
+ "moji": "🎼"
+ },
+ "mute": {
+ "unicode": "1F507",
+ "unicode_alternates": [],
+ "name": "speaker with cancellation stroke",
+ "shortname": ":mute:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sound", "volume"],
+ "moji": "🔇"
+ },
+ "nail_care": {
+ "unicode": "1F485",
+ "unicode_alternates": [],
+ "name": "nail polish",
+ "shortname": ":nail_care:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beauty", "manicure"],
+ "moji": "💅"
+ },
+ "name_badge": {
+ "unicode": "1F4DB",
+ "unicode_alternates": [],
+ "name": "name badge",
+ "shortname": ":name_badge:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fire", "forbid"],
+ "moji": "📛"
+ },
+ "necktie": {
+ "unicode": "1F454",
+ "unicode_alternates": [],
+ "name": "necktie",
+ "shortname": ":necktie:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cloth", "fashion", "formal", "shirt", "suitup"],
+ "moji": "👔"
+ },
+ "negative_squared_cross_mark": {
+ "unicode": "274E",
+ "unicode_alternates": [],
+ "name": "negative squared cross mark",
+ "shortname": ":negative_squared_cross_mark:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["deny", "green-square", "no", "x"],
+ "moji": "❎"
+ },
+ "network": {
+ "unicode": "1F5A7",
+ "unicode_alternates": [],
+ "name": "three networked computers",
+ "shortname": ":network:",
+ "category": "objects_symbols",
+ "aliases": [":three_networked_computers:"],
+ "aliases_ascii": [],
+ "keywords": ["lan", "wan", "network", "technology"]
+ },
+ "neutral_face": {
+ "unicode": "1F610",
+ "unicode_alternates": [],
+ "name": "neutral face",
+ "shortname": ":neutral_face:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "indifference", "neutral", "objective", "impartial", "blank"],
+ "moji": "😐"
+ },
+ "new": {
+ "unicode": "1F195",
+ "unicode_alternates": [],
+ "name": "squared new",
+ "shortname": ":new:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "🆕"
+ },
+ "new_moon": {
+ "unicode": "1F311",
+ "unicode_alternates": [],
+ "name": "new moon symbol",
+ "shortname": ":new_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "new", "sky", "night", "cheese", "phase"],
+ "moji": "🌑"
+ },
+ "new_moon_with_face": {
+ "unicode": "1F31A",
+ "unicode_alternates": [],
+ "name": "new moon with face",
+ "shortname": ":new_moon_with_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "new", "anthropomorphic", "face", "sky", "night", "cheese", "phase"],
+ "moji": "🌚"
+ },
+ "newspaper": {
+ "unicode": "1F4F0",
+ "unicode_alternates": [],
+ "name": "newspaper",
+ "shortname": ":newspaper:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["headline", "press"],
+ "moji": "📰"
+ },
+ "newspaper2": {
+ "unicode": "1F5DE",
+ "unicode_alternates": [],
+ "name": "rolled-up newspaper",
+ "shortname": ":newspaper2:",
+ "category": "objects_symbols",
+ "aliases": [":rolled_up_newspaper:"],
+ "aliases_ascii": [],
+ "keywords": ["headline", "press"]
+ },
+ "night_with_stars": {
+ "unicode": "1F303",
+ "unicode_alternates": [],
+ "name": "night with stars",
+ "shortname": ":night_with_stars:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "star", "cloudless", "evening", "planets", "space", "sky"],
+ "moji": "🌃"
+ },
+ "nine": {
+ "moji": "9️⃣",
+ "unicode": "0039-20E3",
+ "unicode_alternates": ["0039-FE0F-20E3"],
+ "name": "digit nine",
+ "shortname": ":nine:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["9", "blue-square", "numbers"]
+ },
+ "no_bell": {
+ "unicode": "1F515",
+ "unicode_alternates": [],
+ "name": "bell with cancellation stroke",
+ "shortname": ":no_bell:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mute", "sound", "volume"],
+ "moji": "🔕"
+ },
+ "no_bicycles": {
+ "unicode": "1F6B3",
+ "unicode_alternates": [],
+ "name": "no bicycles",
+ "shortname": ":no_bicycles:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cyclist", "prohibited", "bicycle", "bike pedal", "no"],
+ "moji": "🚳"
+ },
+ "no_entry": {
+ "unicode": "26D4",
+ "unicode_alternates": ["26D4-FE0F"],
+ "name": "no entry",
+ "shortname": ":no_entry:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bad", "denied", "limit", "privacy", "security", "stop"],
+ "moji": "⛔"
+ },
+ "no_entry_sign": {
+ "unicode": "1F6AB",
+ "unicode_alternates": [],
+ "name": "no entry sign",
+ "shortname": ":no_entry_sign:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["denied", "disallow", "forbid", "limit", "stop", "no", "stop", "entry"],
+ "moji": "🚫"
+ },
+ "no_good": {
+ "unicode": "1F645",
+ "unicode_alternates": [],
+ "name": "face with no good gesture",
+ "shortname": ":no_good:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman", "no", "stop", "nope", "don&#039;t", "not"],
+ "moji": "🙅"
+ },
+ "no_mobile_phones": {
+ "unicode": "1F4F5",
+ "unicode_alternates": [],
+ "name": "no mobile phones",
+ "shortname": ":no_mobile_phones:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["iphone", "mute"],
+ "moji": "📵"
+ },
+ "no_mouth": {
+ "unicode": "1F636",
+ "unicode_alternates": [],
+ "name": "face without mouth",
+ "shortname": ":no_mouth:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":-X", ":X", ":-#", ":#", "=X", "=x", ":x", ":-x", "=#"],
+ "keywords": ["face", "hellokitty", "mouth", "silent", "vapid"],
+ "moji": "😶"
+ },
+ "no_pedestrians": {
+ "unicode": "1F6B7",
+ "unicode_alternates": [],
+ "name": "no pedestrians",
+ "shortname": ":no_pedestrians:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["crossing", "rules", "walking", "no", "walk", "pedestrian", "stroll", "stride", "foot", "feet"],
+ "moji": "🚷"
+ },
+ "no_smoking": {
+ "unicode": "1F6AD",
+ "unicode_alternates": [],
+ "name": "no smoking symbol",
+ "shortname": ":no_smoking:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cigarette", "no", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"],
+ "moji": "🚭"
+ },
+ "non-potable_water": {
+ "unicode": "1F6B1",
+ "unicode_alternates": [],
+ "name": "non-potable water symbol",
+ "shortname": ":non-potable_water:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["drink", "faucet", "tap", "non-potable", "water", "not drinkable", "dirty", "gross", "aqua", "h20"],
+ "moji": "🚱"
+ },
+ "nose": {
+ "unicode": "1F443",
+ "unicode_alternates": [],
+ "name": "nose",
+ "shortname": ":nose:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["smell", "sniff"],
+ "moji": "👃"
+ },
+ "note": {
+ "unicode": "1F5C9",
+ "unicode_alternates": [],
+ "name": "note page",
+ "shortname": ":note:",
+ "category": "objects_symbols",
+ "aliases": [":note_page:"],
+ "aliases_ascii": [],
+ "keywords": ["stationery", "post-it"]
+ },
+ "note_empty": {
+ "unicode": "1F5C6",
+ "unicode_alternates": [],
+ "name": "empty note page",
+ "shortname": ":note_empty:",
+ "category": "objects_symbols",
+ "aliases": [":empty_note_page:"],
+ "aliases_ascii": [],
+ "keywords": ["stationery", "post-it"]
+ },
+ "notebook": {
+ "unicode": "1F4D3",
+ "unicode_alternates": [],
+ "name": "notebook",
+ "shortname": ":notebook:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["notes", "paper", "record", "stationery"],
+ "moji": "📓"
+ },
+ "notebook_with_decorative_cover": {
+ "unicode": "1F4D4",
+ "unicode_alternates": [],
+ "name": "notebook with decorative cover",
+ "shortname": ":notebook_with_decorative_cover:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["classroom", "notes", "paper", "record"],
+ "moji": "📔"
+ },
+ "notepad": {
+ "unicode": "1F5CA",
+ "unicode_alternates": [],
+ "name": "note pad",
+ "shortname": ":notepad:",
+ "category": "objects_symbols",
+ "aliases": [":note_pad:"],
+ "aliases_ascii": [],
+ "keywords": ["stationery", "post-it"]
+ },
+ "notepad_empty": {
+ "unicode": "1F5C7",
+ "unicode_alternates": [],
+ "name": "empty note pad",
+ "shortname": ":notepad_empty:",
+ "category": "objects_symbols",
+ "aliases": [":empty_note_pad:"],
+ "aliases_ascii": [],
+ "keywords": ["stationery", "post-it"]
+ },
+ "notepad_spiral": {
+ "unicode": "1F5D2",
+ "unicode_alternates": [],
+ "name": "spiral note pad",
+ "shortname": ":notepad_spiral:",
+ "category": "objects_symbols",
+ "aliases": [":spiral_note_pad:"],
+ "aliases_ascii": [],
+ "keywords": ["stationery"]
+ },
+ "notes": {
+ "unicode": "1F3B6",
+ "unicode_alternates": [],
+ "name": "multiple musical notes",
+ "shortname": ":notes:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["music", "score", "musical", "music", "notes", "music", "sound", "melody"],
+ "moji": "🎶"
+ },
+ "nut_and_bolt": {
+ "unicode": "1F529",
+ "unicode_alternates": [],
+ "name": "nut and bolt",
+ "shortname": ":nut_and_bolt:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["handy", "tools"],
+ "moji": "🔩"
+ },
+ "o": {
+ "unicode": "2B55",
+ "unicode_alternates": ["2B55-FE0F"],
+ "name": "heavy large circle",
+ "shortname": ":o:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["circle", "round"],
+ "moji": "⭕"
+ },
+ "o2": {
+ "unicode": "1F17E",
+ "unicode_alternates": [],
+ "name": "negative squared latin capital letter o",
+ "shortname": ":o2:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "letter", "red-square"],
+ "moji": "🅾"
+ },
+ "ocean": {
+ "unicode": "1F30A",
+ "unicode_alternates": [],
+ "name": "water wave",
+ "shortname": ":ocean:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sea", "water", "wave", "ocean", "wave", "surf", "beach", "tide"],
+ "moji": "🌊"
+ },
+ "octopus": {
+ "unicode": "1F419",
+ "unicode_alternates": [],
+ "name": "octopus",
+ "shortname": ":octopus:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "creature", "ocean", "sea"],
+ "moji": "🐙"
+ },
+ "oden": {
+ "unicode": "1F362",
+ "unicode_alternates": [],
+ "name": "oden",
+ "shortname": ":oden:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "japanese", "oden", "seafood", "casserole", "stew"],
+ "moji": "🍢"
+ },
+ "office": {
+ "unicode": "1F3E2",
+ "unicode_alternates": [],
+ "name": "office building",
+ "shortname": ":office:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "bureau", "work"],
+ "moji": "🏢"
+ },
+ "oil": {
+ "unicode": "1F6E2",
+ "unicode_alternates": [],
+ "name": "oil drum",
+ "shortname": ":oil:",
+ "category": "objects_symbols",
+ "aliases": [":oil_drum:"],
+ "aliases_ascii": [],
+ "keywords": ["petroleum"]
+ },
+ "ok": {
+ "unicode": "1F197",
+ "unicode_alternates": [],
+ "name": "squared ok",
+ "shortname": ":ok:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["agree", "blue-square", "good", "yes"],
+ "moji": "🆗"
+ },
+ "ok_hand": {
+ "unicode": "1F44C",
+ "unicode_alternates": [],
+ "name": "ok hand sign",
+ "shortname": ":ok_hand:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fingers", "limbs", "perfect", "okay", "ok", "smoke", "smoking", "marijuana", "joint", "pot", "420"],
+ "moji": "👌"
+ },
+ "ok_woman": {
+ "unicode": "1F646",
+ "unicode_alternates": [],
+ "name": "face with ok gesture",
+ "shortname": ":ok_woman:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["*\\0/*", "\\0/", "*\\O/*", "\\O/"],
+ "keywords": ["female", "girl", "human", "pink", "women", "yes", "ok", "okay", "accept"],
+ "moji": "🙆"
+ },
+ "older_man": {
+ "unicode": "1F474",
+ "unicode_alternates": [],
+ "name": "older man",
+ "shortname": ":older_man:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["human", "male", "men"],
+ "moji": "👴"
+ },
+ "older_woman": {
+ "unicode": "1F475",
+ "unicode_alternates": [],
+ "name": "older woman",
+ "shortname": ":older_woman:",
+ "category": "emoticons",
+ "aliases": [":grandma:"],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "women", "grandma", "grandmother"],
+ "moji": "👵"
+ },
+ "om_symbol": {
+ "unicode": "1F549",
+ "unicode_alternates": [],
+ "name": "om symbol",
+ "shortname": ":om_symbol:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["hinduism", "sound", "spiritual", "icon", "dharmic", "buddhism", "jainism", "meditate"]
+ },
+ "on": {
+ "unicode": "1F51B",
+ "unicode_alternates": [],
+ "name": "on with exclamation mark with left right arrow abo",
+ "shortname": ":on:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "words"],
+ "moji": "🔛"
+ },
+ "oncoming_automobile": {
+ "unicode": "1F698",
+ "unicode_alternates": [],
+ "name": "oncoming automobile",
+ "shortname": ":oncoming_automobile:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["car", "transportation", "vehicle", "sedan", "car", "automobile"],
+ "moji": "🚘"
+ },
+ "oncoming_bus": {
+ "unicode": "1F68D",
+ "unicode_alternates": [],
+ "name": "oncoming bus",
+ "shortname": ":oncoming_bus:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "bus", "school", "city", "transportation", "public"],
+ "moji": "🚍"
+ },
+ "oncoming_police_car": {
+ "unicode": "1F694",
+ "unicode_alternates": [],
+ "name": "oncoming police car",
+ "shortname": ":oncoming_police_car:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["enforcement", "law", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"],
+ "moji": "🚔"
+ },
+ "oncoming_taxi": {
+ "unicode": "1F696",
+ "unicode_alternates": [],
+ "name": "oncoming taxi",
+ "shortname": ":oncoming_taxi:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cars", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"],
+ "moji": "🚖"
+ },
+ "one": {
+ "moji": "1️⃣",
+ "unicode": "0031-20E3",
+ "unicode_alternates": ["0031-FE0F-20E3"],
+ "name": "digit one",
+ "shortname": ":one:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["1", "blue-square", "numbers"]
+ },
+ "open_file_folder": {
+ "unicode": "1F4C2",
+ "unicode_alternates": [],
+ "name": "open file folder",
+ "shortname": ":open_file_folder:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents", "load"],
+ "moji": "📂"
+ },
+ "open_hands": {
+ "unicode": "1F450",
+ "unicode_alternates": [],
+ "name": "open hands sign",
+ "shortname": ":open_hands:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["butterfly", "fingers"],
+ "moji": "👐"
+ },
+ "open_mouth": {
+ "unicode": "1F62E",
+ "unicode_alternates": [],
+ "name": "face with open mouth",
+ "shortname": ":open_mouth:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":-O", ":O", ":-o", ":o", "O_O", ">:O"],
+ "keywords": ["face", "impressed", "mouth", "open", "jaw", "gapping", "surprise", "wow"],
+ "moji": "😮"
+ },
+ "ophiuchus": {
+ "unicode": "26CE",
+ "unicode_alternates": [],
+ "name": "ophiuchus",
+ "shortname": ":ophiuchus:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ophiuchus", "serpent", "snake", "astrology", "greek", "constellation", "stars", "zodiac", "purple-square", "sign", "horoscope"],
+ "moji": "⛎"
+ },
+ "optical_disk": {
+ "unicode": "1F5B8",
+ "unicode_alternates": [],
+ "name": "optical disc icon",
+ "shortname": ":optical_disk:",
+ "category": "objects_symbols",
+ "aliases": [":optical_disc_icon:"],
+ "aliases_ascii": [],
+ "keywords": ["cd", "dvd", "disc", "disk", "technology"]
+ },
+ "orange_book": {
+ "unicode": "1F4D9",
+ "unicode_alternates": [],
+ "name": "orange book",
+ "shortname": ":orange_book:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["knowledge", "library", "read"],
+ "moji": "📙"
+ },
+ "outbox_tray": {
+ "unicode": "1F4E4",
+ "unicode_alternates": [],
+ "name": "outbox tray",
+ "shortname": ":outbox_tray:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["email", "inbox"],
+ "moji": "📤"
+ },
+ "ox": {
+ "unicode": "1F402",
+ "unicode_alternates": [],
+ "name": "ox",
+ "shortname": ":ox:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "beef", "cow"],
+ "moji": "🐂"
+ },
+ "package": {
+ "unicode": "1F4E6",
+ "unicode_alternates": [],
+ "name": "package",
+ "shortname": ":package:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["gift", "mail"],
+ "moji": "📦"
+ },
+ "page": {
+ "unicode": "1F5CF",
+ "unicode_alternates": [],
+ "name": "page",
+ "shortname": ":page:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["document"]
+ },
+ "page_facing_up": {
+ "unicode": "1F4C4",
+ "unicode_alternates": [],
+ "name": "page facing up",
+ "shortname": ":page_facing_up:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents"],
+ "moji": "📄"
+ },
+ "page_with_curl": {
+ "unicode": "1F4C3",
+ "unicode_alternates": [],
+ "name": "page with curl",
+ "shortname": ":page_with_curl:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents"],
+ "moji": "📃"
+ },
+ "pager": {
+ "unicode": "1F4DF",
+ "unicode_alternates": [],
+ "name": "pager",
+ "shortname": ":pager:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bbcall", "oldschool"],
+ "moji": "📟"
+ },
+ "pages": {
+ "unicode": "1F5D0",
+ "unicode_alternates": [],
+ "name": "pages",
+ "shortname": ":pages:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents"]
+ },
+ "paintbrush": {
+ "unicode": "1F58C",
+ "unicode_alternates": [],
+ "name": "lower left paintbrush",
+ "shortname": ":paintbrush:",
+ "category": "objects_symbols",
+ "aliases": [":lower_left_paintbrush:"],
+ "aliases_ascii": [],
+ "keywords": ["brush", "art", "painting"]
+ },
+ "palm_tree": {
+ "unicode": "1F334",
+ "unicode_alternates": [],
+ "name": "palm tree",
+ "shortname": ":palm_tree:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "vegetable", "palm", "tree", "coconuts", "fronds", "warm", "tropical"],
+ "moji": "🌴"
+ },
+ "panda_face": {
+ "unicode": "1F43C",
+ "unicode_alternates": [],
+ "name": "panda face",
+ "shortname": ":panda_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "panda", "bear", "face", "cub", "cute", "endearment", "friendship", "love", "bamboo", "china", "black", "white"],
+ "moji": "🐼"
+ },
+ "paperclip": {
+ "unicode": "1F4CE",
+ "unicode_alternates": [],
+ "name": "paperclip",
+ "shortname": ":paperclip:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents", "stationery"],
+ "moji": "📎"
+ },
+ "paperclips": {
+ "unicode": "1F587",
+ "unicode_alternates": [],
+ "name": "linked paperclips",
+ "shortname": ":paperclips:",
+ "category": "objects_symbols",
+ "aliases": [":linked_paperclips:"],
+ "aliases_ascii": [],
+ "keywords": ["documents", "stationery"]
+ },
+ "park": {
+ "unicode": "1F3DE",
+ "unicode_alternates": [],
+ "name": "national park",
+ "shortname": ":park:",
+ "category": "travel_places",
+ "aliases": [":national_park:"],
+ "aliases_ascii": [],
+ "keywords": ["woods", "nature", "wildlife", "forest", "wilderness", "national"]
+ },
+ "parking": {
+ "unicode": "1F17F",
+ "unicode_alternates": ["1F17F-FE0F"],
+ "name": "negative squared latin capital letter p",
+ "shortname": ":parking:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "blue-square", "cars", "letter"],
+ "moji": "🅿"
+ },
+ "part_alternation_mark": {
+ "unicode": "303D",
+ "unicode_alternates": ["303D-FE0F"],
+ "name": "part alternation mark",
+ "shortname": ":part_alternation_mark:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["graph", "sing", "song", "vocal", "music", "karaoke", "cue", "letter", "m", "japanese"],
+ "moji": "〽"
+ },
+ "partly_sunny": {
+ "unicode": "26C5",
+ "unicode_alternates": ["26C5-FE0F"],
+ "name": "sun behind cloud",
+ "shortname": ":partly_sunny:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cloud", "morning", "nature", "weather"],
+ "moji": "⛅"
+ },
+ "passport_control": {
+ "unicode": "1F6C2",
+ "unicode_alternates": [],
+ "name": "passport control",
+ "shortname": ":passport_control:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "custom", "passport", "official", "travel", "control", "foreign", "identification"],
+ "moji": "🛂"
+ },
+ "peach": {
+ "unicode": "1F351",
+ "unicode_alternates": [],
+ "name": "peach",
+ "shortname": ":peach:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "nature", "peach", "fruit", "juicy", "pit"],
+ "moji": "🍑"
+ },
+ "pear": {
+ "unicode": "1F350",
+ "unicode_alternates": [],
+ "name": "pear",
+ "shortname": ":pear:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fruit", "nature", "pear", "fruit", "shape"],
+ "moji": "🍐"
+ },
+ "pen_ballpoint": {
+ "unicode": "1F58A",
+ "unicode_alternates": [],
+ "name": "lower left ballpoint pen",
+ "shortname": ":pen_ballpoint:",
+ "category": "objects_symbols",
+ "aliases": [":lower_left_ballpoint_pen:"],
+ "aliases_ascii": [],
+ "keywords": ["write", "bic", "ink"]
+ },
+ "pen_fountain": {
+ "unicode": "1F58B",
+ "unicode_alternates": [],
+ "name": "lower left fountain pen",
+ "shortname": ":pen_fountain:",
+ "category": "objects_symbols",
+ "aliases": [":lower_left_fountain_pen:"],
+ "aliases_ascii": [],
+ "keywords": ["write", "calligraphy", "ink"]
+ },
+ "pencil": {
+ "unicode": "1F4DD",
+ "unicode_alternates": [],
+ "name": "memo",
+ "shortname": ":pencil:",
+ "category": "objects",
+ "aliases": [":memo:"],
+ "aliases_ascii": [],
+ "keywords": ["documents", "paper", "station", "write"],
+ "moji": "📝"
+ },
+ "pencil2": {
+ "unicode": "270F",
+ "unicode_alternates": ["270F-FE0F"],
+ "name": "pencil",
+ "shortname": ":pencil2:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["paper", "stationery", "write"],
+ "moji": "✏"
+ },
+ "pencil3": {
+ "unicode": "1F589",
+ "unicode_alternates": [],
+ "name": "lower left pencil",
+ "shortname": ":pencil3:",
+ "category": "objects_symbols",
+ "aliases": [":lower_left_pencil:"],
+ "aliases_ascii": [],
+ "keywords": ["paper", "stationery", "write"]
+ },
+ "penguin": {
+ "unicode": "1F427",
+ "unicode_alternates": [],
+ "name": "penguin",
+ "shortname": ":penguin:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐧"
+ },
+ "pennant_black": {
+ "unicode": "1F3F2",
+ "unicode_alternates": [],
+ "name": "black pennant",
+ "shortname": ":pennant_black:",
+ "category": "objects_symbols",
+ "aliases": [":black_pennant:"],
+ "aliases_ascii": [],
+ "keywords": ["flag", "athletics"]
+ },
+ "pennant_white": {
+ "unicode": "1F3F1",
+ "unicode_alternates": [],
+ "name": "white pennant",
+ "shortname": ":pennant_white:",
+ "category": "objects_symbols",
+ "aliases": [":white_pennant:"],
+ "aliases_ascii": [],
+ "keywords": ["flag", "athletics"]
+ },
+ "pensive": {
+ "unicode": "1F614",
+ "unicode_alternates": [],
+ "name": "pensive face",
+ "shortname": ":pensive:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "okay", "sad", "pensive", "thoughtful", "think", "reflective", "wistful", "meditate", "serious"],
+ "moji": "😔"
+ },
+ "performing_arts": {
+ "unicode": "1F3AD",
+ "unicode_alternates": [],
+ "name": "performing arts",
+ "shortname": ":performing_arts:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["acting", "drama", "theater", "performing", "arts", "performance", "entertainment", "acting", "story", "mask", "masks"],
+ "moji": "🎭"
+ },
+ "persevere": {
+ "unicode": "1F623",
+ "unicode_alternates": [],
+ "name": "persevering face",
+ "shortname": ":persevere:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [">.<"],
+ "keywords": ["endure", "persevere", "face", "no", "sick", "upset"],
+ "moji": "😣"
+ },
+ "person_frowning": {
+ "unicode": "1F64D",
+ "unicode_alternates": [],
+ "name": "person frowning",
+ "shortname": ":person_frowning:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman", "dejected", "rejected", "sad", "frown"],
+ "moji": "🙍"
+ },
+ "person_with_blond_hair": {
+ "unicode": "1F471",
+ "unicode_alternates": [],
+ "name": "person with blond hair",
+ "shortname": ":person_with_blond_hair:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["male", "man", "blonde", "young", "western", "westerner", "occidental"],
+ "moji": "👱"
+ },
+ "person_with_pouting_face": {
+ "unicode": "1F64E",
+ "unicode_alternates": [],
+ "name": "person with pouting face",
+ "shortname": ":person_with_pouting_face:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman", "pout", "sexy", "cute", "annoyed"],
+ "moji": "🙎"
+ },
+ "pig": {
+ "unicode": "1F437",
+ "unicode_alternates": [],
+ "name": "pig face",
+ "shortname": ":pig:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "oink"],
+ "moji": "🐷"
+ },
+ "pig2": {
+ "unicode": "1F416",
+ "unicode_alternates": [],
+ "name": "pig",
+ "shortname": ":pig2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "pig", "piggy", "pork", "ham", "hog", "bacon", "oink", "slop", "livestock", "greed", "greedy"],
+ "moji": "🐖"
+ },
+ "pig_nose": {
+ "unicode": "1F43D",
+ "unicode_alternates": [],
+ "name": "pig nose",
+ "shortname": ":pig_nose:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "oink", "pig", "nose", "snout", "food", "eat", "cute", "oink", "pink", "smell", "truffle"],
+ "moji": "🐽"
+ },
+ "pill": {
+ "unicode": "1F48A",
+ "unicode_alternates": [],
+ "name": "pill",
+ "shortname": ":pill:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["health", "medicine"],
+ "moji": "💊"
+ },
+ "pineapple": {
+ "unicode": "1F34D",
+ "unicode_alternates": [],
+ "name": "pineapple",
+ "shortname": ":pineapple:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "nature", "pineapple", "pina", "tropical", "flower"],
+ "moji": "🍍"
+ },
+ "piracy": {
+ "unicode": "1F572",
+ "unicode_alternates": [],
+ "name": "no piracy",
+ "shortname": ":piracy:",
+ "category": "objects_symbols",
+ "aliases": [":no_piracy:"],
+ "aliases_ascii": [],
+ "keywords": ["theft", "rule"]
+ },
+ "pisces": {
+ "unicode": "2653",
+ "unicode_alternates": ["2653-FE0F"],
+ "name": "pisces",
+ "shortname": ":pisces:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["pisces", "fish", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "purple-square", "sign", "zodiac", "horoscope"],
+ "moji": "♓"
+ },
+ "pizza": {
+ "unicode": "1F355",
+ "unicode_alternates": [],
+ "name": "slice of pizza",
+ "shortname": ":pizza:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "party", "pizza", "pie", "new york", "italian", "italy", "slice", "peperoni"],
+ "moji": "🍕"
+ },
+ "point_down": {
+ "unicode": "1F447",
+ "unicode_alternates": [],
+ "name": "white down pointing backhand index",
+ "shortname": ":point_down:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["direction", "fingers", "hand"],
+ "moji": "👇"
+ },
+ "point_left": {
+ "unicode": "1F448",
+ "unicode_alternates": [],
+ "name": "white left pointing backhand index",
+ "shortname": ":point_left:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["direction", "fingers", "hand"],
+ "moji": "👈"
+ },
+ "point_right": {
+ "unicode": "1F449",
+ "unicode_alternates": [],
+ "name": "white right pointing backhand index",
+ "shortname": ":point_right:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["direction", "fingers", "hand"],
+ "moji": "👉"
+ },
+ "point_up": {
+ "unicode": "261D",
+ "unicode_alternates": ["261D-FE0F"],
+ "name": "white up pointing index",
+ "shortname": ":point_up:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["direction", "fingers", "hand"],
+ "moji": "☝"
+ },
+ "point_up_2": {
+ "unicode": "1F446",
+ "unicode_alternates": [],
+ "name": "white up pointing backhand index",
+ "shortname": ":point_up_2:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["direction", "fingers", "hand"],
+ "moji": "👆"
+ },
+ "police_car": {
+ "unicode": "1F693",
+ "unicode_alternates": [],
+ "name": "police car",
+ "shortname": ":police_car:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cars", "enforcement", "law", "transportation", "vehicle", "police", "car", "emergency", "ticket", "citation", "crime", "help", "officer"],
+ "moji": "🚓"
+ },
+ "poodle": {
+ "unicode": "1F429",
+ "unicode_alternates": [],
+ "name": "poodle",
+ "shortname": ":poodle:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["101", "animal", "dog", "nature", "poodle", "dog", "clip", "showy", "sophisticated", "vain"],
+ "moji": "🐩"
+ },
+ "poop": {
+ "unicode": "1F4A9",
+ "unicode_alternates": [],
+ "name": "pile of poo",
+ "shortname": ":poop:",
+ "category": "emoticons",
+ "aliases": [":shit:", ":hankey:", ":poo:"],
+ "aliases_ascii": [],
+ "keywords": ["poop", "shit", "shitface", "turd", "poo"],
+ "moji": "💩"
+ },
+ "post_office": {
+ "unicode": "1F3E3",
+ "unicode_alternates": [],
+ "name": "japanese post office",
+ "shortname": ":post_office:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "communication", "email"],
+ "moji": "🏣"
+ },
+ "postal_horn": {
+ "unicode": "1F4EF",
+ "unicode_alternates": [],
+ "name": "postal horn",
+ "shortname": ":postal_horn:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["instrument", "music"],
+ "moji": "📯"
+ },
+ "postbox": {
+ "unicode": "1F4EE",
+ "unicode_alternates": [],
+ "name": "postbox",
+ "shortname": ":postbox:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["email", "envelope", "letter"],
+ "moji": "📮"
+ },
+ "potable_water": {
+ "unicode": "1F6B0",
+ "unicode_alternates": [],
+ "name": "potable water symbol",
+ "shortname": ":potable_water:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "cleaning", "faucet", "liquid", "restroom", "potable", "water", "drinkable", "pure", "clear", "clean", "aqua", "h20"],
+ "moji": "🚰"
+ },
+ "pouch": {
+ "unicode": "1F45D",
+ "unicode_alternates": [],
+ "name": "pouch",
+ "shortname": ":pouch:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accessories", "bag", "pouch", "bag", "cosmetic", "packing", "grandma", "makeup"],
+ "moji": "👝"
+ },
+ "poultry_leg": {
+ "unicode": "1F357",
+ "unicode_alternates": [],
+ "name": "poultry leg",
+ "shortname": ":poultry_leg:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "meat", "poultry", "leg", "chicken", "fried"],
+ "moji": "🍗"
+ },
+ "pound": {
+ "unicode": "1F4B7",
+ "unicode_alternates": [],
+ "name": "banknote with pound sign",
+ "shortname": ":pound:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bills", "british", "currency", "england", "money", "sterling", "uk", "pound", "britain", "british", "banknote", "money", "currency", "paper", "cash", "bills"],
+ "moji": "💷"
+ },
+ "pouting_cat": {
+ "unicode": "1F63E",
+ "unicode_alternates": [],
+ "name": "pouting cat face",
+ "shortname": ":pouting_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "pout", "annoyed", "miffed", "glower", "frown"],
+ "moji": "😾"
+ },
+ "pray": {
+ "unicode": "1F64F",
+ "unicode_alternates": [],
+ "name": "person with folded hands",
+ "shortname": ":pray:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["highfive", "hope", "namaste", "please", "wish", "pray", "high five", "hands", "sorrow", "regret", "sorry"],
+ "moji": "🙏"
+ },
+ "princess": {
+ "unicode": "1F478",
+ "unicode_alternates": [],
+ "name": "princess",
+ "shortname": ":princess:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blond", "crown", "female", "girl", "woman", "princess", "royal", "royalty", "king", "queen", "daughter", "disney", "high-maintenance"],
+ "moji": "👸"
+ },
+ "printer": {
+ "unicode": "1F5A8",
+ "unicode_alternates": [],
+ "name": "printer",
+ "shortname": ":printer:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["hardcopy", "paper", "inkjet", "laser"]
+ },
+ "prohibited": {
+ "unicode": "1F6C7",
+ "unicode_alternates": [],
+ "name": "prohibited sign",
+ "shortname": ":prohibited:",
+ "category": "objects_symbols",
+ "aliases": [":prohibited_sign:"],
+ "aliases_ascii": [],
+ "keywords": ["no", "not", "denied", "disallow", "forbid", "limit", "stop"]
+ },
+ "projector": {
+ "unicode": "1F4FD",
+ "unicode_alternates": [],
+ "name": "film projector",
+ "shortname": ":projector:",
+ "category": "objects_symbols",
+ "aliases": [":film_projector:"],
+ "aliases_ascii": [],
+ "keywords": ["movie", "video", "motion", "picture", "8mm", "16mm"]
+ },
+ "punch": {
+ "unicode": "1F44A",
+ "unicode_alternates": [],
+ "name": "fisted hand sign",
+ "shortname": ":punch:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fist", "hand"],
+ "moji": "👊"
+ },
+ "purple_heart": {
+ "unicode": "1F49C",
+ "unicode_alternates": [],
+ "name": "purple heart",
+ "shortname": ":purple_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines", "purple", "violet", "heart", "love", "sensitive", "understanding", "compassionate", "compassion", "duty", "honor", "royalty", "veteran", "sacrifice"],
+ "moji": "💜"
+ },
+ "purse": {
+ "unicode": "1F45B",
+ "unicode_alternates": [],
+ "name": "purse",
+ "shortname": ":purse:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accessories", "fashion", "money", "purse", "clutch", "bag", "handbag", "coin bag", "accessory", "money", "ladies", "shopping"],
+ "moji": "👛"
+ },
+ "pushpin": {
+ "unicode": "1F4CC",
+ "unicode_alternates": [],
+ "name": "pushpin",
+ "shortname": ":pushpin:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["stationery"],
+ "moji": "📌"
+ },
+ "pushpin_black": {
+ "unicode": "1F588",
+ "unicode_alternates": [],
+ "name": "black pushpin",
+ "shortname": ":pushpin_black:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["stationery"]
+ },
+ "put_litter_in_its_place": {
+ "unicode": "1F6AE",
+ "unicode_alternates": [],
+ "name": "put litter in its place symbol",
+ "shortname": ":put_litter_in_its_place:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "litter", "waste", "trash", "garbage", "receptacle", "can"],
+ "moji": "🚮"
+ },
+ "question": {
+ "unicode": "2753",
+ "unicode_alternates": [],
+ "name": "black question mark ornament",
+ "shortname": ":question:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["confused", "doubt"],
+ "moji": "❓"
+ },
+ "rabbit": {
+ "unicode": "1F430",
+ "unicode_alternates": [],
+ "name": "rabbit face",
+ "shortname": ":rabbit:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐰"
+ },
+ "rabbit2": {
+ "unicode": "1F407",
+ "unicode_alternates": [],
+ "name": "rabbit",
+ "shortname": ":rabbit2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "rabbit", "bunny", "easter", "reproduction", "prolific"],
+ "moji": "🐇"
+ },
+ "race_car": {
+ "unicode": "1F3CE",
+ "unicode_alternates": [],
+ "name": "racing car",
+ "shortname": ":race_car:",
+ "category": "activity",
+ "aliases": [":racing_car:"],
+ "aliases_ascii": [],
+ "keywords": ["formula 1", "race", "stock", "nascar", "speed", "drive"]
+ },
+ "racehorse": {
+ "unicode": "1F40E",
+ "unicode_alternates": [],
+ "name": "horse",
+ "shortname": ":racehorse:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "gamble", "horse", "powerful", "draft", "calvary", "cowboy", "cowgirl", "mounted", "race", "ride", "gallop", "trot", "colt", "filly", "mare", "stallion", "gelding", "yearling", "thoroughbred", "pony"],
+ "moji": "🐎"
+ },
+ "radio": {
+ "unicode": "1F4FB",
+ "unicode_alternates": [],
+ "name": "radio",
+ "shortname": ":radio:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "music", "podcast", "program"],
+ "moji": "📻"
+ },
+ "radio_button": {
+ "unicode": "1F518",
+ "unicode_alternates": [],
+ "name": "radio button",
+ "shortname": ":radio_button:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["input"],
+ "moji": "🔘"
+ },
+ "rage": {
+ "unicode": "1F621",
+ "unicode_alternates": [],
+ "name": "pouting face",
+ "shortname": ":rage:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["angry", "despise", "hate", "mad", "pout", "anger", "rage", "irate"],
+ "moji": "😡"
+ },
+ "railway_car": {
+ "unicode": "1F683",
+ "unicode_alternates": [],
+ "name": "railway car",
+ "shortname": ":railway_car:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "railway", "rail", "car", "coach", "train"],
+ "moji": "🚃"
+ },
+ "railway_track": {
+ "unicode": "1F6E4",
+ "unicode_alternates": [],
+ "name": "railway track",
+ "shortname": ":railway_track:",
+ "category": "travel_places",
+ "aliases": [":railroad_track:"],
+ "aliases_ascii": [],
+ "keywords": ["train", "trolley", "subway", "locomotive", "transit"]
+ },
+ "rainbow": {
+ "unicode": "1F308",
+ "unicode_alternates": [],
+ "name": "rainbow",
+ "shortname": ":rainbow:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["happy", "nature", "photo", "sky", "unicorn", "rainbow", "color", "pride", "diversity", "spectrum", "refract", "leprechaun", "gold"],
+ "moji": "🌈"
+ },
+ "raised_hand": {
+ "unicode": "270B",
+ "unicode_alternates": [],
+ "name": "raised hand",
+ "shortname": ":raised_hand:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman"],
+ "moji": "✋"
+ },
+ "raised_hands": {
+ "unicode": "1F64C",
+ "unicode_alternates": [],
+ "name": "person raising both hands in celebration",
+ "shortname": ":raised_hands:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["gesture", "hooray", "winning", "woot", "yay", "banzai"],
+ "moji": "🙌"
+ },
+ "raising_hand": {
+ "unicode": "1F64B",
+ "unicode_alternates": [],
+ "name": "happy person raising one hand",
+ "shortname": ":raising_hand:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girl", "woman", "hand", "raise", "notice", "attention", "answer"],
+ "moji": "🙋"
+ },
+ "ram": {
+ "unicode": "1F40F",
+ "unicode_alternates": [],
+ "name": "ram",
+ "shortname": ":ram:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "sheep", "ram", "sheep", "male", "horn", "horns"],
+ "moji": "🐏"
+ },
+ "ramen": {
+ "unicode": "1F35C",
+ "unicode_alternates": [],
+ "name": "steaming bowl",
+ "shortname": ":ramen:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chipsticks", "food", "japanese", "noodle", "ramen", "noodles", "bowl", "steaming", "soup"],
+ "moji": "🍜"
+ },
+ "rat": {
+ "unicode": "1F400",
+ "unicode_alternates": [],
+ "name": "rat",
+ "shortname": ":rat:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "mouse", "rat", "rodent", "crooked", "snitch"],
+ "moji": "🐀"
+ },
+ "recycle": {
+ "unicode": "267B",
+ "unicode_alternates": ["267B-FE0F"],
+ "name": "black universal recycling symbol",
+ "shortname": ":recycle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "environment", "garbage", "trash"],
+ "moji": "♻"
+ },
+ "red_car": {
+ "unicode": "1F697",
+ "unicode_alternates": [],
+ "name": "automobile",
+ "shortname": ":red_car:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle"],
+ "moji": "🚗"
+ },
+ "red_circle": {
+ "unicode": "1F534",
+ "unicode_alternates": [],
+ "name": "large red circle",
+ "shortname": ":red_circle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔴"
+ },
+ "registered": {
+ "moji": "®",
+ "unicode": "00AE",
+ "unicode_alternates": [],
+ "name": "registered sign",
+ "shortname": ":registered:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alphabet", "circle"]
+ },
+ "relaxed": {
+ "unicode": "263A",
+ "unicode_alternates": ["263A-FE0F"],
+ "name": "white smiling face",
+ "shortname": ":relaxed:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blush", "face", "happiness", "massage", "smile"],
+ "moji": "☺"
+ },
+ "relieved": {
+ "unicode": "1F60C",
+ "unicode_alternates": [],
+ "name": "relieved face",
+ "shortname": ":relieved:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "happiness", "massage", "phew", "relaxed", "relieved", "satisfied", "phew", "relief"],
+ "moji": "😌"
+ },
+ "reminder_ribbon": {
+ "unicode": "1F397",
+ "unicode_alternates": [],
+ "name": "reminder ribbon",
+ "shortname": ":reminder_ribbon:",
+ "category": "celebration",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["awareness"]
+ },
+ "repeat": {
+ "unicode": "1F501",
+ "unicode_alternates": [],
+ "name": "clockwise rightwards and leftwards open circle arr",
+ "shortname": ":repeat:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["loop", "record"],
+ "moji": "🔁"
+ },
+ "repeat_one": {
+ "unicode": "1F502",
+ "unicode_alternates": [],
+ "name": "clockwise rightwards and leftwards open circle arr",
+ "shortname": ":repeat_one:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "loop"],
+ "moji": "🔂"
+ },
+ "restroom": {
+ "unicode": "1F6BB",
+ "unicode_alternates": [],
+ "name": "restroom",
+ "shortname": ":restroom:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "woman", "man", "unisex", "bathroom", "restroom", "sign", "shared", "toilet"],
+ "moji": "🚻"
+ },
+ "revolving_hearts": {
+ "unicode": "1F49E",
+ "unicode_alternates": [],
+ "name": "revolving hearts",
+ "shortname": ":revolving_hearts:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "revolving", "moving", "circle", "multiple", "lovers"],
+ "moji": "💞"
+ },
+ "rewind": {
+ "unicode": "23EA",
+ "unicode_alternates": [],
+ "name": "black left-pointing double triangle",
+ "shortname": ":rewind:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "play"],
+ "moji": "⏪"
+ },
+ "ribbon": {
+ "unicode": "1F380",
+ "unicode_alternates": [],
+ "name": "ribbon",
+ "shortname": ":ribbon:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bowtie", "decoration", "girl", "pink", "ribbon", "lace", "wrap", "decorate"],
+ "moji": "🎀"
+ },
+ "rice": {
+ "unicode": "1F35A",
+ "unicode_alternates": [],
+ "name": "cooked rice",
+ "shortname": ":rice:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "rice", "white", "grain", "food", "bowl"],
+ "moji": "🍚"
+ },
+ "rice_ball": {
+ "unicode": "1F359",
+ "unicode_alternates": [],
+ "name": "rice ball",
+ "shortname": ":rice_ball:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "japanese", "rice", "ball", "white", "nori", "seaweed", "japanese"],
+ "moji": "🍙"
+ },
+ "rice_cracker": {
+ "unicode": "1F358",
+ "unicode_alternates": [],
+ "name": "rice cracker",
+ "shortname": ":rice_cracker:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "japanese", "rice", "cracker", "seaweed", "food", "japanese"],
+ "moji": "🍘"
+ },
+ "rice_scene": {
+ "unicode": "1F391",
+ "unicode_alternates": [],
+ "name": "moon viewing ceremony",
+ "shortname": ":rice_scene:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo", "moon", "viewing", "observing", "otsukimi", "tsukimi", "rice", "scene", "festival", "autumn"],
+ "moji": "🎑"
+ },
+ "right_speaker": {
+ "unicode": "1F568",
+ "unicode_alternates": [],
+ "name": "right speaker",
+ "shortname": ":right_speaker:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sound", "listen", "hear", "noise", "volume"]
+ },
+ "right_speaker_one": {
+ "unicode": "1F569",
+ "unicode_alternates": [],
+ "name": "right speaker with one sound wave",
+ "shortname": ":right_speaker_one:",
+ "category": "objects_symbols",
+ "aliases": [":right_speaker_with_one_sound_wave:"],
+ "aliases_ascii": [],
+ "keywords": ["low", "volume"]
+ },
+ "right_speaker_three": {
+ "unicode": "1F56A",
+ "unicode_alternates": [],
+ "name": "right speaker with three sound waves",
+ "shortname": ":right_speaker_three:",
+ "category": "objects_symbols",
+ "aliases": [":right_speaker_with_three_sound_waves:"],
+ "aliases_ascii": [],
+ "keywords": ["loud", "high", "volume"]
+ },
+ "ring": {
+ "unicode": "1F48D",
+ "unicode_alternates": [],
+ "name": "ring",
+ "shortname": ":ring:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["marriage", "propose", "valentines", "wedding"],
+ "moji": "💍"
+ },
+ "ringing_bell": {
+ "unicode": "1F56D",
+ "unicode_alternates": [],
+ "name": "ringing bell",
+ "shortname": ":ringing_bell:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alert", "ding", "volume", "sound", "chime"]
+ },
+ "rocket": {
+ "unicode": "1F680",
+ "unicode_alternates": [],
+ "name": "rocket",
+ "shortname": ":rocket:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["launch", "ship", "staffmode", "rocket", "space", "spacecraft", "astronaut", "cosmonaut"],
+ "moji": "🚀"
+ },
+ "roller_coaster": {
+ "unicode": "1F3A2",
+ "unicode_alternates": [],
+ "name": "roller coaster",
+ "shortname": ":roller_coaster:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["carnival", "fun", "photo", "play", "playground", "roller", "coaster", "amusement", "park", "fair", "ride", "entertainment"],
+ "moji": "🎢"
+ },
+ "rooster": {
+ "unicode": "1F413",
+ "unicode_alternates": [],
+ "name": "rooster",
+ "shortname": ":rooster:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "chicken", "nature", "rooster", "cockerel", "cock", "male", "cock-a-doodle-doo", "crowing"],
+ "moji": "🐓"
+ },
+ "rose": {
+ "unicode": "1F339",
+ "unicode_alternates": [],
+ "name": "rose",
+ "shortname": ":rose:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flowers", "love", "valentines", "rose", "fragrant", "flower", "thorns", "love", "petals", "romance"],
+ "moji": "🌹"
+ },
+ "rosette": {
+ "unicode": "1F3F5",
+ "unicode_alternates": [],
+ "name": "rosette",
+ "shortname": ":rosette:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flower"]
+ },
+ "rosette_black": {
+ "unicode": "1F3F6",
+ "unicode_alternates": [],
+ "name": "black rosette",
+ "shortname": ":rosette_black:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flower"]
+ },
+ "rotating_light": {
+ "unicode": "1F6A8",
+ "unicode_alternates": [],
+ "name": "police cars revolving light",
+ "shortname": ":rotating_light:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["911", "ambulance", "emergency", "police", "light", "police", "emergency"],
+ "moji": "🚨"
+ },
+ "round_pushpin": {
+ "unicode": "1F4CD",
+ "unicode_alternates": [],
+ "name": "round pushpin",
+ "shortname": ":round_pushpin:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["stationery"],
+ "moji": "📍"
+ },
+ "rowboat": {
+ "unicode": "1F6A3",
+ "unicode_alternates": [],
+ "name": "rowboat",
+ "shortname": ":rowboat:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["hobby", "ship", "sports", "water", "boat", "row", "oar", "paddle"],
+ "moji": "🚣"
+ },
+ "rugby_football": {
+ "unicode": "1F3C9",
+ "unicode_alternates": [],
+ "name": "rugby football",
+ "shortname": ":rugby_football:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sports", "rugby", "football", "ball", "sport", "team", "england"],
+ "moji": "🏉"
+ },
+ "runner": {
+ "unicode": "1F3C3",
+ "unicode_alternates": [],
+ "name": "runner",
+ "shortname": ":runner:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["exercise", "man", "walking", "run", "runner", "jog", "exercise", "sprint", "race", "dash"],
+ "moji": "🏃"
+ },
+ "running_shirt_with_sash": {
+ "unicode": "1F3BD",
+ "unicode_alternates": [],
+ "name": "running shirt with sash",
+ "shortname": ":running_shirt_with_sash:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["pageant", "play", "running", "run", "shirt", "cloths", "compete", "sports"],
+ "moji": "🎽"
+ },
+ "sagittarius": {
+ "unicode": "2650",
+ "unicode_alternates": ["2650-FE0F"],
+ "name": "sagittarius",
+ "shortname": ":sagittarius:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sagittarius", "centaur", "archer", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "moji": "♐"
+ },
+ "sailboat": {
+ "unicode": "26F5",
+ "unicode_alternates": ["26F5-FE0F"],
+ "name": "sailboat",
+ "shortname": ":sailboat:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ship", "transportation"],
+ "moji": "⛵"
+ },
+ "sake": {
+ "unicode": "1F376",
+ "unicode_alternates": [],
+ "name": "sake bottle and cup",
+ "shortname": ":sake:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beverage", "drink", "drunk", "wine", "sake", "wine", "rice", "ferment", "alcohol", "japanese", "drink"],
+ "moji": "🍶"
+ },
+ "sandal": {
+ "unicode": "1F461",
+ "unicode_alternates": [],
+ "name": "womans sandal",
+ "shortname": ":sandal:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "shoes"],
+ "moji": "👡"
+ },
+ "santa": {
+ "unicode": "1F385",
+ "unicode_alternates": [],
+ "name": "father christmas",
+ "shortname": ":santa:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["christmas", "father christmas", "festival", "male", "man", "xmas", "santa", "saint nick", "jolly", "ho ho ho", "north pole", "presents", "gifts", "naughty", "nice", "sleigh", "father", "christmas", "holiday"],
+ "moji": "🎅"
+ },
+ "satellite": {
+ "unicode": "1F4E1",
+ "unicode_alternates": [],
+ "name": "satellite antenna",
+ "shortname": ":satellite:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication"],
+ "moji": "📡"
+ },
+ "satellite_orbital": {
+ "unicode": "1F6F0",
+ "unicode_alternates": [],
+ "name": "satellite",
+ "shortname": ":satellite_orbital:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "orbital", "space"]
+ },
+ "saxophone": {
+ "unicode": "1F3B7",
+ "unicode_alternates": [],
+ "name": "saxophone",
+ "shortname": ":saxophone:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["instrument", "music", "saxophone", "sax", "music", "instrument", "woodwind"],
+ "moji": "🎷"
+ },
+ "school": {
+ "unicode": "1F3EB",
+ "unicode_alternates": [],
+ "name": "school",
+ "shortname": ":school:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["building", "school", "university", "elementary", "middle", "high", "college", "teach", "education"],
+ "moji": "🏫"
+ },
+ "school_satchel": {
+ "unicode": "1F392",
+ "unicode_alternates": [],
+ "name": "school satchel",
+ "shortname": ":school_satchel:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bag", "education", "student", "school", "satchel", "backpack", "bag", "packing", "pack", "hike", "education", "adventure", "travel", "sightsee"],
+ "moji": "🎒"
+ },
+ "scissors": {
+ "unicode": "2702",
+ "unicode_alternates": ["2702-FE0F"],
+ "name": "black scissors",
+ "shortname": ":scissors:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cut", "stationery"],
+ "moji": "✂"
+ },
+ "scorpius": {
+ "unicode": "264F",
+ "unicode_alternates": ["264F-FE0F"],
+ "name": "scorpius",
+ "shortname": ":scorpius:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["scorpius", "scorpion", "scorpio", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "sign", "zodiac", "horoscope"],
+ "moji": "♏"
+ },
+ "scream": {
+ "unicode": "1F631",
+ "unicode_alternates": [],
+ "name": "face screaming in fear",
+ "shortname": ":scream:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "munch", "scream", "painting", "artist", "alien"],
+ "moji": "😱"
+ },
+ "scream_cat": {
+ "unicode": "1F640",
+ "unicode_alternates": [],
+ "name": "weary cat face",
+ "shortname": ":scream_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "munch", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted", "scream", "painting", "artist"],
+ "moji": "🙀"
+ },
+ "scroll": {
+ "unicode": "1F4DC",
+ "unicode_alternates": [],
+ "name": "scroll",
+ "shortname": ":scroll:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["documents"],
+ "moji": "📜"
+ },
+ "seat": {
+ "unicode": "1F4BA",
+ "unicode_alternates": [],
+ "name": "seat",
+ "shortname": ":seat:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sit"],
+ "moji": "💺"
+ },
+ "secret": {
+ "unicode": "3299",
+ "unicode_alternates": ["3299-FE0F"],
+ "name": "circled ideograph secret",
+ "shortname": ":secret:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["privacy"],
+ "moji": "㊙"
+ },
+ "see_no_evil": {
+ "unicode": "1F648",
+ "unicode_alternates": [],
+ "name": "see-no-evil monkey",
+ "shortname": ":see_no_evil:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "monkey", "nature", "monkey", "see", "eyes", "vision", "sight", "mizaru"],
+ "moji": "🙈"
+ },
+ "seedling": {
+ "unicode": "1F331",
+ "unicode_alternates": [],
+ "name": "seedling",
+ "shortname": ":seedling:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["grass", "lawn", "nature", "plant", "seedling", "plant", "new", "start", "grow"],
+ "moji": "🌱"
+ },
+ "seven": {
+ "moji": "7️⃣",
+ "unicode": "0037-20E3",
+ "unicode_alternates": ["0037-FE0F-20E3"],
+ "name": "digit seven",
+ "shortname": ":seven:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["7", "blue-square", "numbers", "prime"]
+ },
+ "shaved_ice": {
+ "unicode": "1F367",
+ "unicode_alternates": [],
+ "name": "shaved ice",
+ "shortname": ":shaved_ice:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["desert", "hot", "shaved", "ice", "dessert", "treat", "syrup", "flavoring"],
+ "moji": "🍧"
+ },
+ "sheep": {
+ "unicode": "1F411",
+ "unicode_alternates": [],
+ "name": "sheep",
+ "shortname": ":sheep:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "sheep", "wool", "flock", "follower", "ewe", "female", "lamb"],
+ "moji": "🐑"
+ },
+ "shell": {
+ "unicode": "1F41A",
+ "unicode_alternates": [],
+ "name": "spiral shell",
+ "shortname": ":shell:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beach", "nature", "sea", "shell", "spiral", "beach", "sand", "crab", "nautilus"],
+ "moji": "🐚"
+ },
+ "shield": {
+ "unicode": "1F6E1",
+ "unicode_alternates": [],
+ "name": "shield",
+ "shortname": ":shield:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["interstate", "route", "sign", "highway", "interstate"]
+ },
+ "ship": {
+ "unicode": "1F6A2",
+ "unicode_alternates": [],
+ "name": "ship",
+ "shortname": ":ship:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["titanic", "transportation", "ferry", "ship", "boat"],
+ "moji": "🚢"
+ },
+ "shirt": {
+ "unicode": "1F455",
+ "unicode_alternates": [],
+ "name": "t-shirt",
+ "shortname": ":shirt:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cloth", "fashion"],
+ "moji": "👕"
+ },
+ "shopping_bags": {
+ "unicode": "1F6CD",
+ "unicode_alternates": [],
+ "name": "shopping bags",
+ "shortname": ":shopping_bags:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["purchase", "mall", "buy", "store", "shop"]
+ },
+ "shower": {
+ "unicode": "1F6BF",
+ "unicode_alternates": [],
+ "name": "shower",
+ "shortname": ":shower:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bath", "clean", "wash", "bathroom", "shower", "soap", "water", "clean", "shampoo", "lather"],
+ "moji": "🚿"
+ },
+ "signal_strength": {
+ "unicode": "1F4F6",
+ "unicode_alternates": [],
+ "name": "antenna with bars",
+ "shortname": ":signal_strength:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "📶"
+ },
+ "six": {
+ "moji": "6️⃣",
+ "unicode": "0036-20E3",
+ "unicode_alternates": ["0036-FE0F-20E3"],
+ "name": "digit six",
+ "shortname": ":six:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["6", "blue-square", "numbers"]
+ },
+ "six_pointed_star": {
+ "unicode": "1F52F",
+ "unicode_alternates": [],
+ "name": "six pointed star with middle dot",
+ "shortname": ":six_pointed_star:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["purple-square"],
+ "moji": "🔯"
+ },
+ "ski": {
+ "unicode": "1F3BF",
+ "unicode_alternates": [],
+ "name": "ski and ski boot",
+ "shortname": ":ski:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cold", "sports", "winter", "ski", "downhill", "cross-country", "poles", "snow", "winter", "mountain", "alpine", "powder", "slalom", "freestyle"],
+ "moji": "🎿"
+ },
+ "skull": {
+ "unicode": "1F480",
+ "unicode_alternates": [],
+ "name": "skull",
+ "shortname": ":skull:",
+ "category": "emoticons",
+ "aliases": [":skeleton:"],
+ "aliases_ascii": [],
+ "keywords": ["dead", "skeleton", "dying"],
+ "moji": "💀"
+ },
+ "sleeping": {
+ "unicode": "1F634",
+ "unicode_alternates": [],
+ "name": "sleeping face",
+ "shortname": ":sleeping:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "sleepy", "tired", "sleep", "sleepy", "sleeping", "snore"],
+ "moji": "😴"
+ },
+ "sleeping_accommodation": {
+ "unicode": "1F6CC",
+ "unicode_alternates": [],
+ "name": "sleeping accommodation",
+ "shortname": ":sleeping_accommodation:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["hotel", "motel", "rest"]
+ },
+ "sleepy": {
+ "unicode": "1F62A",
+ "unicode_alternates": [],
+ "name": "sleepy face",
+ "shortname": ":sleepy:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "rest", "tired", "sleepy", "tired", "exhausted"],
+ "moji": "😪"
+ },
+ "slight_frown": {
+ "unicode": "1F641",
+ "unicode_alternates": [],
+ "name": "slightly frowning face",
+ "shortname": ":slight_frown:",
+ "category": "people",
+ "aliases": [":slightly_frowning_face:"],
+ "aliases_ascii": [],
+ "keywords": ["slight", "frown", "unhappy", "disappointed"]
+ },
+ "slight_smile": {
+ "unicode": "1F642",
+ "unicode_alternates": [],
+ "name": "slightly smiling face",
+ "shortname": ":slight_smile:",
+ "category": "people",
+ "aliases": [":slightly_smiling_face:"],
+ "aliases_ascii": [],
+ "keywords": ["slight", "smile", "happy"]
+ },
+ "slot_machine": {
+ "unicode": "1F3B0",
+ "unicode_alternates": [],
+ "name": "slot machine",
+ "shortname": ":slot_machine:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bet", "gamble", "vegas", "slot", "machine", "gamble", "one-armed bandit", "slots", "luck"],
+ "moji": "🎰"
+ },
+ "small_blue_diamond": {
+ "unicode": "1F539",
+ "unicode_alternates": [],
+ "name": "small blue diamond",
+ "shortname": ":small_blue_diamond:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔹"
+ },
+ "small_orange_diamond": {
+ "unicode": "1F538",
+ "unicode_alternates": [],
+ "name": "small orange diamond",
+ "shortname": ":small_orange_diamond:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔸"
+ },
+ "small_red_triangle": {
+ "unicode": "1F53A",
+ "unicode_alternates": [],
+ "name": "up-pointing red triangle",
+ "shortname": ":small_red_triangle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔺"
+ },
+ "small_red_triangle_down": {
+ "unicode": "1F53B",
+ "unicode_alternates": [],
+ "name": "down-pointing red triangle",
+ "shortname": ":small_red_triangle_down:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔻"
+ },
+ "smile": {
+ "unicode": "1F604",
+ "unicode_alternates": [],
+ "name": "smiling face with open mouth and smiling eyes",
+ "shortname": ":smile:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":)", ":-)", "=]", "=)", ":]"],
+ "keywords": ["face", "funny", "haha", "happy", "joy", "laugh", "smile", "smiley", "smiling"],
+ "moji": "😄"
+ },
+ "smile_cat": {
+ "unicode": "1F638",
+ "unicode_alternates": [],
+ "name": "grinning cat face with smiling eyes",
+ "shortname": ":smile_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "cat", "smile", "grin", "grinning"],
+ "moji": "😸"
+ },
+ "smiley": {
+ "unicode": "1F603",
+ "unicode_alternates": [],
+ "name": "smiling face with open mouth",
+ "shortname": ":smiley:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":D", ":-D", "=D"],
+ "keywords": ["face", "haha", "happy", "joy", "smiling", "smile", "smiley"],
+ "moji": "😃"
+ },
+ "smiley_cat": {
+ "unicode": "1F63A",
+ "unicode_alternates": [],
+ "name": "smiling cat face with open mouth",
+ "shortname": ":smiley_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "happy", "smile", "smiley", "cat", "happy"],
+ "moji": "😺"
+ },
+ "smiling_imp": {
+ "unicode": "1F608",
+ "unicode_alternates": [],
+ "name": "smiling face with horns",
+ "shortname": ":smiling_imp:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["devil", "horns", "horns", "devil", "impish", "trouble"],
+ "moji": "😈"
+ },
+ "smirk": {
+ "unicode": "1F60F",
+ "unicode_alternates": [],
+ "name": "smirking face",
+ "shortname": ":smirk:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mean", "prank", "smile", "smug", "smirking", "smirk", "smug", "smile", "half-smile", "conceited"],
+ "moji": "😏"
+ },
+ "smirk_cat": {
+ "unicode": "1F63C",
+ "unicode_alternates": [],
+ "name": "cat face with wry smile",
+ "shortname": ":smirk_cat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cats", "smirk", "smirking", "wry", "confident", "confidence"],
+ "moji": "😼"
+ },
+ "smoking": {
+ "unicode": "1F6AC",
+ "unicode_alternates": [],
+ "name": "smoking symbol",
+ "shortname": ":smoking:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cigarette", "kills", "tobacco", "smoking", "cigarette", "smoke", "cancer", "lungs", "inhale", "tar", "nicotine"],
+ "moji": "🚬"
+ },
+ "snail": {
+ "unicode": "1F40C",
+ "unicode_alternates": [],
+ "name": "snail",
+ "shortname": ":snail:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "shell", "slow", "snail", "slow", "escargot", "french", "appetizer"],
+ "moji": "🐌"
+ },
+ "snake": {
+ "unicode": "1F40D",
+ "unicode_alternates": [],
+ "name": "snake",
+ "shortname": ":snake:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "evil"],
+ "moji": "🐍"
+ },
+ "snowboarder": {
+ "unicode": "1F3C2",
+ "unicode_alternates": [],
+ "name": "snowboarder",
+ "shortname": ":snowboarder:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sports", "winter", "snow", "boarding", "sports", "freestyle", "halfpipe", "board", "mountain", "alpine", "winter"],
+ "moji": "🏂"
+ },
+ "snowflake": {
+ "unicode": "2744",
+ "unicode_alternates": ["2744-FE0F"],
+ "name": "snowflake",
+ "shortname": ":snowflake:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas", "snowflake", "snow", "frozen", "droplet", "ice", "crystal", "cold", "chilly", "winter", "unique", "special", "below zero", "elsa"],
+ "moji": "❄"
+ },
+ "snowman": {
+ "unicode": "26C4",
+ "unicode_alternates": ["26C4-FE0F"],
+ "name": "snowman without snow",
+ "shortname": ":snowman:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["christmas", "cold", "season", "weather", "winter", "xmas"],
+ "moji": "⛄"
+ },
+ "sob": {
+ "unicode": "1F62D",
+ "unicode_alternates": [],
+ "name": "loudly crying face",
+ "shortname": ":sob:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cry", "face", "sad", "tears", "upset", "cry", "sob", "tears", "sad", "melancholy", "morn", "somber", "hurt"],
+ "moji": "😭"
+ },
+ "soccer": {
+ "unicode": "26BD",
+ "unicode_alternates": ["26BD-FE0F"],
+ "name": "soccer ball",
+ "shortname": ":soccer:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["balls", "fifa", "football", "sports", "european", "football"],
+ "moji": "⚽"
+ },
+ "soon": {
+ "unicode": "1F51C",
+ "unicode_alternates": [],
+ "name": "soon with rightwards arrow above",
+ "shortname": ":soon:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arrow", "words"],
+ "moji": "🔜"
+ },
+ "sos": {
+ "unicode": "1F198",
+ "unicode_alternates": [],
+ "name": "squared sos",
+ "shortname": ":sos:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["emergency", "help", "red-square", "words"],
+ "moji": "🆘"
+ },
+ "sound": {
+ "unicode": "1F509",
+ "unicode_alternates": [],
+ "name": "speaker with one sound wave",
+ "shortname": ":sound:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["speaker", "volume"],
+ "moji": "🔉"
+ },
+ "space_invader": {
+ "unicode": "1F47E",
+ "unicode_alternates": [],
+ "name": "alien monster",
+ "shortname": ":space_invader:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arcade", "game"],
+ "moji": "👾"
+ },
+ "spades": {
+ "unicode": "2660",
+ "unicode_alternates": ["2660-FE0F"],
+ "name": "black spade suit",
+ "shortname": ":spades:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cards", "poker"],
+ "moji": "♠"
+ },
+ "spaghetti": {
+ "unicode": "1F35D",
+ "unicode_alternates": [],
+ "name": "spaghetti",
+ "shortname": ":spaghetti:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "italian", "noodle", "spaghetti", "noodles", "tomato", "sauce", "italian"],
+ "moji": "🍝"
+ },
+ "sparkle": {
+ "unicode": "2747",
+ "unicode_alternates": ["2747-FE0F"],
+ "name": "sparkle",
+ "shortname": ":sparkle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["green-square", "stars"],
+ "moji": "❇"
+ },
+ "sparkler": {
+ "unicode": "1F387",
+ "unicode_alternates": [],
+ "name": "firework sparkler",
+ "shortname": ":sparkler:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "shine", "stars"],
+ "moji": "🎇"
+ },
+ "sparkles": {
+ "unicode": "2728",
+ "unicode_alternates": [],
+ "name": "sparkles",
+ "shortname": ":sparkles:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cool", "shine", "shiny", "stars"],
+ "moji": "✨"
+ },
+ "sparkling_heart": {
+ "unicode": "1F496",
+ "unicode_alternates": [],
+ "name": "sparkling heart",
+ "shortname": ":sparkling_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines"],
+ "moji": "💖"
+ },
+ "speak_no_evil": {
+ "unicode": "1F64A",
+ "unicode_alternates": [],
+ "name": "speak-no-evil monkey",
+ "shortname": ":speak_no_evil:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "monkey", "monkey", "mouth", "talk", "say", "words", "verbal", "verbalize", "oral", "iwazaru"],
+ "moji": "🙊"
+ },
+ "speaker": {
+ "unicode": "1F508",
+ "unicode_alternates": [],
+ "name": "speaker",
+ "shortname": ":speaker:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sound", "listen", "hear", "noise"]
+ },
+ "speaking_head": {
+ "unicode": "1F5E3",
+ "unicode_alternates": [],
+ "name": "speaking head in silhouette",
+ "shortname": ":speaking_head:",
+ "category": "objects_symbols",
+ "aliases": [":speaking_head_in_silhouette:"],
+ "aliases_ascii": [],
+ "keywords": ["talk"]
+ },
+ "speech_balloon": {
+ "unicode": "1F4AC",
+ "unicode_alternates": [],
+ "name": "speech balloon",
+ "shortname": ":speech_balloon:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bubble", "words", "speech", "balloon", "talk", "conversation", "communication", "comic", "dialogue"],
+ "moji": "💬"
+ },
+ "speech_left": {
+ "unicode": "1F5E8",
+ "unicode_alternates": [],
+ "name": "left speech bubble",
+ "shortname": ":speech_left:",
+ "category": "objects_symbols",
+ "aliases": [":left_speech_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ },
+ "speech_right": {
+ "unicode": "1F5E9",
+ "unicode_alternates": [],
+ "name": "right speech bubble",
+ "shortname": ":speech_right:",
+ "category": "objects_symbols",
+ "aliases": [":right_speech_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ },
+ "speech_three": {
+ "unicode": "1F5EB",
+ "unicode_alternates": [],
+ "name": "three speech bubbles",
+ "shortname": ":speech_three:",
+ "category": "objects_symbols",
+ "aliases": [":three_speech_bubbles:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ },
+ "speech_two": {
+ "unicode": "1F5EA",
+ "unicode_alternates": [],
+ "name": "two speech bubbles",
+ "shortname": ":speech_two:",
+ "category": "objects_symbols",
+ "aliases": [":two_speech_bubbles:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "words", "talk", "conversation", "communication", "comic", "dialogue"]
+ },
+ "speedboat": {
+ "unicode": "1F6A4",
+ "unicode_alternates": [],
+ "name": "speedboat",
+ "shortname": ":speedboat:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ship", "transportation", "vehicle", "motor", "speed", "ski", "power", "boat"],
+ "moji": "🚤"
+ },
+ "spider": {
+ "unicode": "1F577",
+ "unicode_alternates": [],
+ "name": "spider",
+ "shortname": ":spider:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["arachnid", "eight-legged"]
+ },
+ "spider_web": {
+ "unicode": "1F578",
+ "unicode_alternates": [],
+ "name": "spider web",
+ "shortname": ":spider_web:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cobweb"]
+ },
+ "spy": {
+ "unicode": "1F575",
+ "unicode_alternates": [],
+ "name": "sleuth or spy",
+ "shortname": ":spy:",
+ "category": "people",
+ "aliases": [":sleuth_or_spy:"],
+ "aliases_ascii": [],
+ "keywords": ["pi", "undercover", "investigator"]
+ },
+ "stadium": {
+ "unicode": "1F3DF",
+ "unicode_alternates": [],
+ "name": "stadium",
+ "shortname": ":stadium:",
+ "category": "travel_places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sport", "event", "concert", "convention", "game"]
+ },
+ "star": {
+ "unicode": "2B50",
+ "unicode_alternates": ["2B50-FE0F"],
+ "name": "white medium star",
+ "shortname": ":star:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "yellow"],
+ "moji": "⭐"
+ },
+ "star2": {
+ "unicode": "1F31F",
+ "unicode_alternates": [],
+ "name": "glowing star",
+ "shortname": ":star2:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "sparkle", "glow", "glowing", "star", "five", "points", "classic"],
+ "moji": "🌟"
+ },
+ "stars": {
+ "unicode": "1F320",
+ "unicode_alternates": [],
+ "name": "shooting star",
+ "shortname": ":stars:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["night", "photo", "shooting", "shoot", "star", "sky", "night", "comet", "meteoroid"],
+ "moji": "🌠"
+ },
+ "station": {
+ "unicode": "1F689",
+ "unicode_alternates": [],
+ "name": "station",
+ "shortname": ":station:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["public", "transportation", "vehicle", "station", "train", "subway"],
+ "moji": "🚉"
+ },
+ "statue_of_liberty": {
+ "unicode": "1F5FD",
+ "unicode_alternates": [],
+ "name": "statue of liberty",
+ "shortname": ":statue_of_liberty:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["american", "newyork"],
+ "moji": "🗽"
+ },
+ "steam_locomotive": {
+ "unicode": "1F682",
+ "unicode_alternates": [],
+ "name": "steam locomotive",
+ "shortname": ":steam_locomotive:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["train", "transportation", "vehicle", "locomotive", "steam", "train", "engine"],
+ "moji": "🚂"
+ },
+ "stereo": {
+ "unicode": "1F4FE",
+ "unicode_alternates": [],
+ "name": "portable stereo",
+ "shortname": ":stereo:",
+ "category": "objects_symbols",
+ "aliases": [":portable_stereo:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "music", "program", "boom", "box"]
+ },
+ "stew": {
+ "unicode": "1F372",
+ "unicode_alternates": [],
+ "name": "pot of food",
+ "shortname": ":stew:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "meat", "stew", "hearty", "soup", "thick", "hot", "pot"],
+ "moji": "🍲"
+ },
+ "stock_chart": {
+ "unicode": "1F5E0",
+ "unicode_alternates": [],
+ "name": "stock chart",
+ "shortname": ":stock_chart:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["graph", "presentation", "stats", "business"]
+ },
+ "straight_ruler": {
+ "unicode": "1F4CF",
+ "unicode_alternates": [],
+ "name": "straight ruler",
+ "shortname": ":straight_ruler:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["stationery"],
+ "moji": "📏"
+ },
+ "strawberry": {
+ "unicode": "1F353",
+ "unicode_alternates": [],
+ "name": "strawberry",
+ "shortname": ":strawberry:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "nature", "strawberry", "short", "cake", "berry"],
+ "moji": "🍓"
+ },
+ "stuck_out_tongue": {
+ "unicode": "1F61B",
+ "unicode_alternates": [],
+ "name": "face with stuck-out tongue",
+ "shortname": ":stuck_out_tongue:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [":P", ":-P", "=P", ":-p", ":p", "=p", ":-Þ", ":Þ", ":þ", ":-þ", ":-b", ":b", "d:"],
+ "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "silly", "playful", "cheeky"],
+ "moji": "😛"
+ },
+ "stuck_out_tongue_closed_eyes": {
+ "unicode": "1F61D",
+ "unicode_alternates": [],
+ "name": "face with stuck-out tongue and tightly-closed eyes",
+ "shortname": ":stuck_out_tongue_closed_eyes:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "mischievous", "playful", "prank", "tongue", "kidding", "silly", "playful", "ecstatic"],
+ "moji": "😝"
+ },
+ "stuck_out_tongue_winking_eye": {
+ "unicode": "1F61C",
+ "unicode_alternates": [],
+ "name": "face with stuck-out tongue and winking eye",
+ "shortname": ":stuck_out_tongue_winking_eye:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [">:P", "X-P", "x-p"],
+ "keywords": ["childish", "face", "mischievous", "playful", "prank", "tongue", "wink", "winking", "kidding", "silly", "playful", "crazy"],
+ "moji": "😜"
+ },
+ "sun_with_face": {
+ "unicode": "1F31E",
+ "unicode_alternates": [],
+ "name": "sun with face",
+ "shortname": ":sun_with_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["morning", "sun", "anthropomorphic", "face", "sky"],
+ "moji": "🌞"
+ },
+ "sunflower": {
+ "unicode": "1F33B",
+ "unicode_alternates": [],
+ "name": "sunflower",
+ "shortname": ":sunflower:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "sunflower", "sun", "flower", "seeds", "yellow"],
+ "moji": "🌻"
+ },
+ "sunglasses": {
+ "unicode": "1F60E",
+ "unicode_alternates": [],
+ "name": "smiling face with sunglasses",
+ "shortname": ":sunglasses:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["B-)", "B)", "8)", "8-)", "B-D", "8-D"],
+ "keywords": ["cool", "face", "smiling", "sunglasses", "sun", "glasses", "sunny", "cool", "smooth"],
+ "moji": "😎"
+ },
+ "sunny": {
+ "unicode": "2600",
+ "unicode_alternates": ["2600-FE0F"],
+ "name": "black sun with rays",
+ "shortname": ":sunny:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["brightness", "weather"]
+ },
+ "sunrise": {
+ "unicode": "1F305",
+ "unicode_alternates": [],
+ "name": "sunrise",
+ "shortname": ":sunrise:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["morning", "photo", "vacation", "view", "sunrise", "sun", "morning", "color", "sky"],
+ "moji": "🌅"
+ },
+ "sunrise_over_mountains": {
+ "unicode": "1F304",
+ "unicode_alternates": [],
+ "name": "sunrise over mountains",
+ "shortname": ":sunrise_over_mountains:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["photo", "vacation", "view", "sunrise", "sun", "morning", "mountain", "rural", "color", "sky"],
+ "moji": "🌄"
+ },
+ "surfer": {
+ "unicode": "1F3C4",
+ "unicode_alternates": [],
+ "name": "surfer",
+ "shortname": ":surfer:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ocean", "sea", "sports", "surfer", "surf", "wave", "ocean", "ride", "swell"],
+ "moji": "🏄"
+ },
+ "sushi": {
+ "unicode": "1F363",
+ "unicode_alternates": [],
+ "name": "sushi",
+ "shortname": ":sushi:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "japanese", "sushi", "fish", "raw", "nigiri", "japanese"],
+ "moji": "🍣"
+ },
+ "suspension_railway": {
+ "unicode": "1F69F",
+ "unicode_alternates": [],
+ "name": "suspension railway",
+ "shortname": ":suspension_railway:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "suspension", "railway", "rail", "train", "transportation"],
+ "moji": "🚟"
+ },
+ "sweat": {
+ "unicode": "1F613",
+ "unicode_alternates": [],
+ "name": "face with cold sweat",
+ "shortname": ":sweat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["':(", "':-(", "'=("],
+ "keywords": ["cold", "sweat", "sick", "anxious", "worried", "clammy", "diaphoresis", "face", "hot"],
+ "moji": "😓"
+ },
+ "sweat_drops": {
+ "unicode": "1F4A6",
+ "unicode_alternates": [],
+ "name": "splashing sweat symbol",
+ "shortname": ":sweat_drops:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["water"],
+ "moji": "💦"
+ },
+ "sweat_smile": {
+ "unicode": "1F605",
+ "unicode_alternates": [],
+ "name": "smiling face with open mouth and cold sweat",
+ "shortname": ":sweat_smile:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": ["':)", "':-)", "'=)", "':D", "':-D", "'=D"],
+ "keywords": ["face", "happy", "hot", "smiling", "cold", "sweat", "perspiration"],
+ "moji": "😅"
+ },
+ "sweet_potato": {
+ "unicode": "1F360",
+ "unicode_alternates": [],
+ "name": "roasted sweet potato",
+ "shortname": ":sweet_potato:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "nature", "sweet", "potato", "potassium", "roasted", "roast"],
+ "moji": "🍠"
+ },
+ "swimmer": {
+ "unicode": "1F3CA",
+ "unicode_alternates": [],
+ "name": "swimmer",
+ "shortname": ":swimmer:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sports", "swimmer", "swim", "water", "pool", "laps", "freestyle", "butterfly", "breaststroke", "backstroke"],
+ "moji": "🏊"
+ },
+ "symbols": {
+ "unicode": "1F523",
+ "unicode_alternates": [],
+ "name": "input symbol for symbols",
+ "shortname": ":symbols:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "🔣"
+ },
+ "syringe": {
+ "unicode": "1F489",
+ "unicode_alternates": [],
+ "name": "syringe",
+ "shortname": ":syringe:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blood", "drugs", "health", "hospital", "medicine", "needle"],
+ "moji": "💉"
+ },
+ "tada": {
+ "unicode": "1F389",
+ "unicode_alternates": [],
+ "name": "party popper",
+ "shortname": ":tada:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["contulations", "party", "party", "popper", "tada", "celebration", "victory", "announcement", "climax", "congratulations"],
+ "moji": "🎉"
+ },
+ "tanabata_tree": {
+ "unicode": "1F38B",
+ "unicode_alternates": [],
+ "name": "tanabata tree",
+ "shortname": ":tanabata_tree:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "plant", "tanabata", "tree", "festival", "star", "wish", "holiday"],
+ "moji": "🎋"
+ },
+ "tangerine": {
+ "unicode": "1F34A",
+ "unicode_alternates": [],
+ "name": "tangerine",
+ "shortname": ":tangerine:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "nature", "tangerine", "citrus", "orange"],
+ "moji": "🍊"
+ },
+ "taurus": {
+ "unicode": "2649",
+ "unicode_alternates": ["2649-FE0F"],
+ "name": "taurus",
+ "shortname": ":taurus:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["purple-square", "sign", "taurus", "bull", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"],
+ "moji": "♉"
+ },
+ "taxi": {
+ "unicode": "1F695",
+ "unicode_alternates": [],
+ "name": "taxi",
+ "shortname": ":taxi:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cars", "transportation", "uber", "vehicle", "taxi", "car", "automobile", "city", "transport", "service"],
+ "moji": "🚕"
+ },
+ "tea": {
+ "unicode": "1F375",
+ "unicode_alternates": [],
+ "name": "teacup without handle",
+ "shortname": ":tea:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bowl", "breakfast", "british", "drink", "green", "tea", "leaf", "drink", "teacup", "hot", "beverage"],
+ "moji": "🍵"
+ },
+ "telephone": {
+ "unicode": "260E",
+ "unicode_alternates": ["260E-FE0F"],
+ "name": "black telephone",
+ "shortname": ":telephone:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "dial", "technology"],
+ "moji": "☎"
+ },
+ "telephone_black": {
+ "unicode": "1F57F",
+ "unicode_alternates": [],
+ "name": "black touchtone telephone",
+ "shortname": ":telephone_black:",
+ "category": "objects_symbols",
+ "aliases": [":black_touchtone_telephone:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "dial", "technology"]
+ },
+ "telephone_receiver": {
+ "unicode": "1F4DE",
+ "unicode_alternates": [],
+ "name": "telephone receiver",
+ "shortname": ":telephone_receiver:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["communication", "dial", "technology"],
+ "moji": "📞"
+ },
+ "telephone_white": {
+ "unicode": "1F57E",
+ "unicode_alternates": [],
+ "name": "white touchtone telephone",
+ "shortname": ":telephone_white:",
+ "category": "objects_symbols",
+ "aliases": [":white_touchtone_telephone:"],
+ "aliases_ascii": [],
+ "keywords": ["communication", "dial", "technology"]
+ },
+ "telescope": {
+ "unicode": "1F52D",
+ "unicode_alternates": [],
+ "name": "telescope",
+ "shortname": ":telescope:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["space", "stars"],
+ "moji": "🔭"
+ },
+ "tennis": {
+ "unicode": "1F3BE",
+ "unicode_alternates": [],
+ "name": "tennis racquet and ball",
+ "shortname": ":tennis:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["balls", "green", "sports", "tennis", "racket", "racquet", "ball", "game", "net", "court", "love"],
+ "moji": "🎾"
+ },
+ "tent": {
+ "unicode": "26FA",
+ "unicode_alternates": ["26FA-FE0F"],
+ "name": "tent",
+ "shortname": ":tent:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["camp", "outdoors", "photo"],
+ "moji": "⛺"
+ },
+ "thermometer": {
+ "unicode": "1F321",
+ "unicode_alternates": [],
+ "name": "thermometer",
+ "shortname": ":thermometer:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["temperature"]
+ },
+ "thought_balloon": {
+ "unicode": "1F4AD",
+ "unicode_alternates": [],
+ "name": "thought balloon",
+ "shortname": ":thought_balloon:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bubble", "cloud", "speech", "thought", "balloon", "comic", "think", "day dream", "wonder"],
+ "moji": "💭"
+ },
+ "thought_left": {
+ "unicode": "1F5EC",
+ "unicode_alternates": [],
+ "name": "left thought bubble",
+ "shortname": ":thought_left:",
+ "category": "objects_symbols",
+ "aliases": [":left_thought_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"]
+ },
+ "thought_right": {
+ "unicode": "1F5ED",
+ "unicode_alternates": [],
+ "name": "right thought bubble",
+ "shortname": ":thought_right:",
+ "category": "objects_symbols",
+ "aliases": [":right_thought_bubble:"],
+ "aliases_ascii": [],
+ "keywords": ["balloon", "cloud", "comic", "think", "day dream", "wonder"]
+ },
+ "three": {
+ "moji": "3️⃣",
+ "unicode": "0033-20E3",
+ "unicode_alternates": ["0033-FE0F-20E3"],
+ "name": "digit three",
+ "shortname": ":three:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["3", "blue-square", "numbers", "prime"]
+ },
+ "thumbs_down_reverse": {
+ "unicode": "1F593",
+ "unicode_alternates": [],
+ "name": "reversed thumbs down sign",
+ "shortname": ":thumbs_down_reverse:",
+ "category": "people",
+ "aliases": [":reversed_thumbs_down_sign:"],
+ "aliases_ascii": [],
+ "keywords": ["hand", "no", "-1"]
+ },
+ "thumbs_up_reverse": {
+ "unicode": "1F592",
+ "unicode_alternates": [],
+ "name": "reversed thumbs up sign",
+ "shortname": ":thumbs_up_reverse:",
+ "category": "people",
+ "aliases": [":reversed_thumbs_up_sign:"],
+ "aliases_ascii": [],
+ "keywords": ["cool", "hand", "like", "yes", "+1"]
+ },
+ "thumbsdown": {
+ "unicode": "1F44E",
+ "unicode_alternates": [],
+ "name": "thumbs down sign",
+ "shortname": ":thumbsdown:",
+ "category": "emoticons",
+ "aliases": [":-1:"],
+ "aliases_ascii": [],
+ "keywords": ["hand", "no"],
+ "moji": "👎"
+ },
+ "thumbsup": {
+ "unicode": "1F44D",
+ "unicode_alternates": [],
+ "name": "thumbs up sign",
+ "shortname": ":thumbsup:",
+ "category": "emoticons",
+ "aliases": [":+1:"],
+ "aliases_ascii": [],
+ "keywords": ["cool", "hand", "like", "yes"],
+ "moji": "👍"
+ },
+ "ticket": {
+ "unicode": "1F3AB",
+ "unicode_alternates": [],
+ "name": "ticket",
+ "shortname": ":ticket:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["concert", "event", "pass", "ticket", "show", "entertainment", "stub", "admission", "proof", "purchase"],
+ "moji": "🎫"
+ },
+ "tickets": {
+ "unicode": "1F39F",
+ "unicode_alternates": [],
+ "name": "admission tickets",
+ "shortname": ":tickets:",
+ "category": "activity",
+ "aliases": [":admission_tickets:"],
+ "aliases_ascii": [],
+ "keywords": ["concert", "event", "pass", "show", "entertainment", "stub", "proof", "purchase"]
+ },
+ "tiger": {
+ "unicode": "1F42F",
+ "unicode_alternates": [],
+ "name": "tiger face",
+ "shortname": ":tiger:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal"],
+ "moji": "🐯"
+ },
+ "tiger2": {
+ "unicode": "1F405",
+ "unicode_alternates": [],
+ "name": "tiger",
+ "shortname": ":tiger2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "tiger", "cat", "striped", "tony", "tigger", "hobs"],
+ "moji": "🐅"
+ },
+ "tired_face": {
+ "unicode": "1F62B",
+ "unicode_alternates": [],
+ "name": "tired face",
+ "shortname": ":tired_face:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "frustrated", "sick", "upset", "whine", "exhausted", "sleepy", "tired"],
+ "moji": "😫"
+ },
+ "toilet": {
+ "unicode": "1F6BD",
+ "unicode_alternates": [],
+ "name": "toilet",
+ "shortname": ":toilet:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["restroom", "wc", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"],
+ "moji": "🚽"
+ },
+ "tokyo_tower": {
+ "unicode": "1F5FC",
+ "unicode_alternates": [],
+ "name": "tokyo tower",
+ "shortname": ":tokyo_tower:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["japan", "photo"],
+ "moji": "🗼"
+ },
+ "tomato": {
+ "unicode": "1F345",
+ "unicode_alternates": [],
+ "name": "tomato",
+ "shortname": ":tomato:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "nature", "vegetable", "tomato", "fruit", "sauce", "italian"],
+ "moji": "🍅"
+ },
+ "tongue": {
+ "unicode": "1F445",
+ "unicode_alternates": [],
+ "name": "tongue",
+ "shortname": ":tongue:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mouth", "playful", "tongue", "mouth", "taste", "buds", "food", "silly", "playful", "tease", "kiss", "french kiss", "lick", "tasty", "playfulness", "silliness", "intimacy"],
+ "moji": "👅"
+ },
+ "tools": {
+ "unicode": "1F6E0",
+ "unicode_alternates": [],
+ "name": "hammer and wrench",
+ "shortname": ":tools:",
+ "category": "objects_symbols",
+ "aliases": [":hammer_and_wrench:"],
+ "aliases_ascii": [],
+ "keywords": ["tools"]
+ },
+ "top": {
+ "unicode": "1F51D",
+ "unicode_alternates": [],
+ "name": "top with upwards arrow above",
+ "shortname": ":top:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "words"],
+ "moji": "🔝"
+ },
+ "tophat": {
+ "unicode": "1F3A9",
+ "unicode_alternates": [],
+ "name": "top hat",
+ "shortname": ":tophat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["classy", "gentleman", "magic", "top", "hat", "cap", "beaver", "high", "tall", "stove", "pipe", "chimney", "topper", "london", "period piece", "magic", "magician"],
+ "moji": "🎩"
+ },
+ "trackball": {
+ "unicode": "1F5B2",
+ "unicode_alternates": [],
+ "name": "trackball",
+ "shortname": ":trackball:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["input", "device", "gadget"]
+ },
+ "tractor": {
+ "unicode": "1F69C",
+ "unicode_alternates": [],
+ "name": "tractor",
+ "shortname": ":tractor:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["agriculture", "car", "farming", "vehicle", "tractor", "farm", "construction", "machine", "digger"],
+ "moji": "🚜"
+ },
+ "traffic_light": {
+ "unicode": "1F6A5",
+ "unicode_alternates": [],
+ "name": "horizontal traffic light",
+ "shortname": ":traffic_light:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["traffic", "transportation", "traffic", "light", "stop", "go", "yield", "horizontal"],
+ "moji": "🚥"
+ },
+ "train": {
+ "unicode": "1F68B",
+ "unicode_alternates": [],
+ "name": "Tram Car",
+ "shortname": ":train:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["tram", "rail"]
+ },
+ "train2": {
+ "unicode": "1F686",
+ "unicode_alternates": [],
+ "name": "train",
+ "shortname": ":train2:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "train", "locomotive", "rail"],
+ "moji": "🚆"
+ },
+ "train_diesel": {
+ "unicode": "1F6F2",
+ "unicode_alternates": [],
+ "name": "diesel locomotive",
+ "shortname": ":train_diesel:",
+ "category": "travel_places",
+ "aliases": [":diesel_locomotive:"],
+ "aliases_ascii": [],
+ "keywords": ["train", "transportation", "engine", "rail"]
+ },
+ "tram": {
+ "unicode": "1F68A",
+ "unicode_alternates": [],
+ "name": "tram",
+ "shortname": ":tram:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "vehicle", "tram", "transportation", "transport"],
+ "moji": "🚊"
+ },
+ "triangle_round": {
+ "unicode": "1F6C6",
+ "unicode_alternates": [],
+ "name": "triangle with rounded corners",
+ "shortname": ":triangle_round:",
+ "category": "objects_symbols",
+ "aliases": [":triangle_with_rounded_corners:"],
+ "aliases_ascii": [],
+ "keywords": ["caution", "warning", "alert"]
+ },
+ "triangular_flag_on_post": {
+ "unicode": "1F6A9",
+ "unicode_alternates": [],
+ "name": "triangular flag on post",
+ "shortname": ":triangular_flag_on_post:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["triangle", "triangular", "flag", "golf", "post", "flagpole"],
+ "moji": "🚩"
+ },
+ "triangular_ruler": {
+ "unicode": "1F4D0",
+ "unicode_alternates": [],
+ "name": "triangular ruler",
+ "shortname": ":triangular_ruler:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["architect", "math", "sketch", "stationery"],
+ "moji": "📐"
+ },
+ "trident": {
+ "unicode": "1F531",
+ "unicode_alternates": [],
+ "name": "trident emblem",
+ "shortname": ":trident:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["spear", "weapon"],
+ "moji": "🔱"
+ },
+ "triumph": {
+ "unicode": "1F624",
+ "unicode_alternates": [],
+ "name": "face with look of triumph",
+ "shortname": ":triumph:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "gas", "phew", "triumph", "steam", "breath"],
+ "moji": "😤"
+ },
+ "trolleybus": {
+ "unicode": "1F68E",
+ "unicode_alternates": [],
+ "name": "trolleybus",
+ "shortname": ":trolleybus:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bart", "transportation", "vehicle", "trolley", "bus", "city", "transport", "transportation"],
+ "moji": "🚎"
+ },
+ "trophy": {
+ "unicode": "1F3C6",
+ "unicode_alternates": [],
+ "name": "trophy",
+ "shortname": ":trophy:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["award", "ceremony", "contest", "ftw", "place", "win", "trophy", "first", "show", "place", "win", "reward", "achievement", "medal"],
+ "moji": "🏆"
+ },
+ "tropical_drink": {
+ "unicode": "1F379",
+ "unicode_alternates": [],
+ "name": "tropical drink",
+ "shortname": ":tropical_drink:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["beverage", "tropical", "drink", "mixed", "pineapple", "coconut", "pina", "fruit", "umbrella"],
+ "moji": "🍹"
+ },
+ "tropical_fish": {
+ "unicode": "1F420",
+ "unicode_alternates": [],
+ "name": "tropical fish",
+ "shortname": ":tropical_fish:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "swim"],
+ "moji": "🐠"
+ },
+ "truck": {
+ "unicode": "1F69A",
+ "unicode_alternates": [],
+ "name": "delivery truck",
+ "shortname": ":truck:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["cars", "transportation", "truck", "delivery", "package"],
+ "moji": "🚚"
+ },
+ "trumpet": {
+ "unicode": "1F3BA",
+ "unicode_alternates": [],
+ "name": "trumpet",
+ "shortname": ":trumpet:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["brass", "music", "trumpet", "brass", "music", "instrument"],
+ "moji": "🎺"
+ },
+ "tulip": {
+ "unicode": "1F337",
+ "unicode_alternates": [],
+ "name": "tulip",
+ "shortname": ":tulip:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["flowers", "nature", "plant", "tulip", "flower", "bulb", "spring", "easter"],
+ "moji": "🌷"
+ },
+ "turned_ok_hand": {
+ "unicode": "1F58F",
+ "unicode_alternates": [],
+ "name": "turned ok hand sign",
+ "shortname": ":turned_ok_hand:",
+ "category": "people",
+ "aliases": [":turned_ok_hand_sign:"],
+ "aliases_ascii": [],
+ "keywords": ["perfect", "okay"]
+ },
+ "turtle": {
+ "unicode": "1F422",
+ "unicode_alternates": [],
+ "name": "turtle",
+ "shortname": ":turtle:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "slow", "turtle", "shell", "tortoise", "chelonian", "reptile", "slow", "snap", "steady"],
+ "moji": "🐢"
+ },
+ "twisted_rightwards_arrows": {
+ "unicode": "1F500",
+ "unicode_alternates": [],
+ "name": "twisted rightwards arrows",
+ "shortname": ":twisted_rightwards_arrows:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "🔀"
+ },
+ "two": {
+ "moji": "2️⃣",
+ "unicode": "0032-20E3",
+ "unicode_alternates": ["0032-FE0F-20E3"],
+ "name": "digit two",
+ "shortname": ":two:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["2", "blue-square", "numbers", "prime"]
+ },
+ "two_hearts": {
+ "unicode": "1F495",
+ "unicode_alternates": [],
+ "name": "two hearts",
+ "shortname": ":two_hearts:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines", "heart", "hearts", "two", "love", "emotion"],
+ "moji": "💕"
+ },
+ "two_men_holding_hands": {
+ "unicode": "1F46C",
+ "unicode_alternates": [],
+ "name": "two men holding hands",
+ "shortname": ":two_men_holding_hands:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bromance", "couple", "friends", "like", "love", "men", "gay", "homosexual", "friends", "hands", "holding", "team", "unity"],
+ "moji": "👬"
+ },
+ "two_women_holding_hands": {
+ "unicode": "1F46D",
+ "unicode_alternates": [],
+ "name": "two women holding hands",
+ "shortname": ":two_women_holding_hands:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["couple", "female", "friends", "like", "love", "women", "hands", "girlfriends", "friends", "sisters", "mother", "daughter", "gay", "homosexual", "couple", "unity"],
+ "moji": "👭"
+ },
+ "u5272": {
+ "unicode": "1F239",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-5272",
+ "shortname": ":u5272:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "cut", "divide", "kanji", "pink"],
+ "moji": "🈹"
+ },
+ "u5408": {
+ "unicode": "1F234",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-5408",
+ "shortname": ":u5408:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "japanese", "join", "kanji"],
+ "moji": "🈴"
+ },
+ "u55b6": {
+ "unicode": "1F23A",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-55b6",
+ "shortname": ":u55b6:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["japanese", "opening hours"],
+ "moji": "🈺"
+ },
+ "u6307": {
+ "unicode": "1F22F",
+ "unicode_alternates": ["1F22F-FE0F"],
+ "name": "squared cjk unified ideograph-6307",
+ "shortname": ":u6307:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "green-square", "kanji", "point"],
+ "moji": "🈯"
+ },
+ "u6708": {
+ "unicode": "1F237",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-6708",
+ "shortname": ":u6708:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "japanese", "kanji", "moon", "orange-square"],
+ "moji": "🈷"
+ },
+ "u6709": {
+ "unicode": "1F236",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-6709",
+ "shortname": ":u6709:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "have", "kanji", "orange-square"],
+ "moji": "🈶"
+ },
+ "u6e80": {
+ "unicode": "1F235",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-6e80",
+ "shortname": ":u6e80:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "full", "japanese", "kanji", "red-square"],
+ "moji": "🈵"
+ },
+ "u7121": {
+ "unicode": "1F21A",
+ "unicode_alternates": ["1F21A-FE0F"],
+ "name": "squared cjk unified ideograph-7121",
+ "shortname": ":u7121:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "japanese", "kanji", "no", "nothing", "orange-square"],
+ "moji": "🈚"
+ },
+ "u7533": {
+ "unicode": "1F238",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-7533",
+ "shortname": ":u7533:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "japanese", "kanji"],
+ "moji": "🈸"
+ },
+ "u7981": {
+ "unicode": "1F232",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-7981",
+ "shortname": ":u7981:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "forbidden", "japanese", "kanji", "limit", "restricted"],
+ "moji": "🈲"
+ },
+ "u7a7a": {
+ "unicode": "1F233",
+ "unicode_alternates": [],
+ "name": "squared cjk unified ideograph-7a7a",
+ "shortname": ":u7a7a:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["chinese", "empty", "japanese", "kanji"],
+ "moji": "🈳"
+ },
+ "umbrella": {
+ "unicode": "2614",
+ "unicode_alternates": ["2614-FE0F"],
+ "name": "umbrella with rain drops",
+ "shortname": ":umbrella:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["rain", "weather"],
+ "moji": "☔"
+ },
+ "unamused": {
+ "unicode": "1F612",
+ "unicode_alternates": [],
+ "name": "unamused face",
+ "shortname": ":unamused:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["bored", "face", "indifference", "serious", "straight face", "unamused", "not amused", "depressed", "unhappy", "disapprove", "lame"],
+ "moji": "😒"
+ },
+ "underage": {
+ "unicode": "1F51E",
+ "unicode_alternates": [],
+ "name": "no one under eighteen symbol",
+ "shortname": ":underage:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["18", "drink", "night", "pub"],
+ "moji": "🔞"
+ },
+ "unlock": {
+ "unicode": "1F513",
+ "unicode_alternates": [],
+ "name": "open lock",
+ "shortname": ":unlock:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["privacy", "security"],
+ "moji": "🔓"
+ },
+ "up": {
+ "unicode": "1F199",
+ "unicode_alternates": [],
+ "name": "squared up with exclamation mark",
+ "shortname": ":up:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square"],
+ "moji": "🆙"
+ },
+ "v": {
+ "unicode": "270C",
+ "unicode_alternates": ["270C-FE0F"],
+ "name": "victory hand",
+ "shortname": ":v:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fingers", "hand", "ohyeah", "peace", "two", "victory"],
+ "moji": "✌"
+ },
+ "vertical_traffic_light": {
+ "unicode": "1F6A6",
+ "unicode_alternates": [],
+ "name": "vertical traffic light",
+ "shortname": ":vertical_traffic_light:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["transportation", "traffic", "light", "stop", "go", "yield", "vertical"],
+ "moji": "🚦"
+ },
+ "vhs": {
+ "unicode": "1F4FC",
+ "unicode_alternates": [],
+ "name": "videocassette",
+ "shortname": ":vhs:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["oldschool", "record", "video"],
+ "moji": "📼"
+ },
+ "vibration_mode": {
+ "unicode": "1F4F3",
+ "unicode_alternates": [],
+ "name": "vibration mode",
+ "shortname": ":vibration_mode:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["orange-square", "phone"],
+ "moji": "📳"
+ },
+ "video_camera": {
+ "unicode": "1F4F9",
+ "unicode_alternates": [],
+ "name": "video camera",
+ "shortname": ":video_camera:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["film", "record"],
+ "moji": "📹"
+ },
+ "video_game": {
+ "unicode": "1F3AE",
+ "unicode_alternates": [],
+ "name": "video game",
+ "shortname": ":video_game:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["PS4", "console", "controller", "play", "video", "game", "console", "controller", "nintendo", "xbox", "playstation"],
+ "moji": "🎮"
+ },
+ "violin": {
+ "unicode": "1F3BB",
+ "unicode_alternates": [],
+ "name": "violin",
+ "shortname": ":violin:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["instrument", "music", "violin", "fiddle", "music", "instrument"],
+ "moji": "🎻"
+ },
+ "virgo": {
+ "unicode": "264D",
+ "unicode_alternates": ["264D-FE0F"],
+ "name": "virgo",
+ "shortname": ":virgo:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sign", "virgo", "maiden", "astrology", "greek", "constellation", "stars", "zodiac", "sign", "zodiac", "horoscope"],
+ "moji": "♍"
+ },
+ "volcano": {
+ "unicode": "1F30B",
+ "unicode_alternates": [],
+ "name": "volcano",
+ "shortname": ":volcano:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "photo", "volcano", "lava", "magma", "hot", "explode"],
+ "moji": "🌋"
+ },
+ "vs": {
+ "unicode": "1F19A",
+ "unicode_alternates": [],
+ "name": "squared vs",
+ "shortname": ":vs:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["orange-square", "words"],
+ "moji": "🆚"
+ },
+ "vulcan": {
+ "unicode": "1F596",
+ "unicode_alternates": [],
+ "name": "raised hand with part between middle and ring fingers",
+ "shortname": ":vulcan:",
+ "category": "people",
+ "aliases": [":raised_hand_with_part_between_middle_and_ring_fingers:"],
+ "aliases_ascii": [],
+ "keywords": ["vulcan", "spock", "leonard", "nimoy", "star trek", "live long"]
+ },
+ "walking": {
+ "unicode": "1F6B6",
+ "unicode_alternates": [],
+ "name": "pedestrian",
+ "shortname": ":walking:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["human", "man", "walk", "pedestrian", "stroll", "stride", "foot", "feet"],
+ "moji": "🚶"
+ },
+ "waning_crescent_moon": {
+ "unicode": "1F318",
+ "unicode_alternates": [],
+ "name": "waning crescent moon symbol",
+ "shortname": ":waning_crescent_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "crescent", "waning", "sky", "night", "cheese", "phase"],
+ "moji": "🌘"
+ },
+ "waning_gibbous_moon": {
+ "unicode": "1F316",
+ "unicode_alternates": [],
+ "name": "waning gibbous moon symbol",
+ "shortname": ":waning_gibbous_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "waning", "gibbous", "sky", "night", "cheese", "phase"],
+ "moji": "🌖"
+ },
+ "warning": {
+ "unicode": "26A0",
+ "unicode_alternates": ["26A0-FE0F"],
+ "name": "warning sign",
+ "shortname": ":warning:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["exclamation", "wip"],
+ "moji": "⚠"
+ },
+ "wastebasket": {
+ "unicode": "1F5D1",
+ "unicode_alternates": [],
+ "name": "wastebasket",
+ "shortname": ":wastebasket:",
+ "category": "objects_symbols",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["trash", "garbage", "dispose"]
+ },
+ "watch": {
+ "unicode": "231A",
+ "unicode_alternates": ["231A-FE0F"],
+ "name": "watch",
+ "shortname": ":watch:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accessories", "time"],
+ "moji": "⌚"
+ },
+ "water_buffalo": {
+ "unicode": "1F403",
+ "unicode_alternates": [],
+ "name": "water buffalo",
+ "shortname": ":water_buffalo:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "cow", "nature", "ox", "water", "buffalo", "asia", "bovine", "milk", "dairy"],
+ "moji": "🐃"
+ },
+ "watermelon": {
+ "unicode": "1F349",
+ "unicode_alternates": [],
+ "name": "watermelon",
+ "shortname": ":watermelon:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["food", "fruit", "melon", "watermelon", "summer", "fruit", "large"],
+ "moji": "🍉"
+ },
+ "wave": {
+ "unicode": "1F44B",
+ "unicode_alternates": [],
+ "name": "waving hand sign",
+ "shortname": ":wave:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["farewell", "gesture", "goodbye", "hands", "solong"],
+ "moji": "👋"
+ },
+ "wavy_dash": {
+ "unicode": "3030",
+ "unicode_alternates": [],
+ "name": "wavy dash",
+ "shortname": ":wavy_dash:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["draw", "line"],
+ "moji": "〰"
+ },
+ "waxing_crescent_moon": {
+ "unicode": "1F312",
+ "unicode_alternates": [],
+ "name": "waxing crescent moon symbol",
+ "shortname": ":waxing_crescent_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature", "moon", "waxing", "sky", "night", "cheese", "phase"],
+ "moji": "🌒"
+ },
+ "waxing_gibbous_moon": {
+ "unicode": "1F314",
+ "unicode_alternates": [],
+ "name": "waxing gibbous moon symbol",
+ "shortname": ":waxing_gibbous_moon:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["nature"],
+ "moji": "🌔"
+ },
+ "wc": {
+ "unicode": "1F6BE",
+ "unicode_alternates": [],
+ "name": "water closet",
+ "shortname": ":wc:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "restroom", "toilet", "water", "closet", "toilet", "bathroom", "throne", "porcelain", "waste", "flush", "plumbing"],
+ "moji": "🚾"
+ },
+ "weary": {
+ "unicode": "1F629",
+ "unicode_alternates": [],
+ "name": "weary face",
+ "shortname": ":weary:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "frustrated", "sad", "sleepy", "tired", "weary", "sleepy", "tired", "tiredness", "study", "finals", "school", "exhausted"],
+ "moji": "😩"
+ },
+ "wedding": {
+ "unicode": "1F492",
+ "unicode_alternates": [],
+ "name": "wedding",
+ "shortname": ":wedding:",
+ "category": "places",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "bride", "couple", "groom", "like", "love", "marriage"],
+ "moji": "💒"
+ },
+ "whale": {
+ "unicode": "1F433",
+ "unicode_alternates": [],
+ "name": "spouting whale",
+ "shortname": ":whale:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "ocean", "sea"],
+ "moji": "🐳"
+ },
+ "whale2": {
+ "unicode": "1F40B",
+ "unicode_alternates": [],
+ "name": "whale",
+ "shortname": ":whale2:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature", "ocean", "sea", "whale", "blubber", "bloated", "fat", "large", "massive"],
+ "moji": "🐋"
+ },
+ "wheelchair": {
+ "unicode": "267F",
+ "unicode_alternates": ["267F-FE0F"],
+ "name": "wheelchair symbol",
+ "shortname": ":wheelchair:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "disabled"],
+ "moji": "♿"
+ },
+ "white_check_mark": {
+ "unicode": "2705",
+ "unicode_alternates": [],
+ "name": "white heavy check mark",
+ "shortname": ":white_check_mark:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["agree", "green-square", "ok"],
+ "moji": "✅"
+ },
+ "white_circle": {
+ "unicode": "26AA",
+ "unicode_alternates": ["26AA-FE0F"],
+ "name": "medium white circle",
+ "shortname": ":white_circle:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "⚪"
+ },
+ "white_flower": {
+ "unicode": "1F4AE",
+ "unicode_alternates": [],
+ "name": "white flower",
+ "shortname": ":white_flower:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["japanese", "white", "flower", "teacher", "school", "grade", "score", "brilliance", "intelligence", "homework", "student", "assignment", "praise"],
+ "moji": "💮"
+ },
+ "white_large_square": {
+ "unicode": "2B1C",
+ "unicode_alternates": ["2B1C-FE0F"],
+ "name": "white large square",
+ "shortname": ":white_large_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "⬜"
+ },
+ "white_medium_small_square": {
+ "unicode": "25FD",
+ "unicode_alternates": ["25FD-FE0F"],
+ "name": "white medium small square",
+ "shortname": ":white_medium_small_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "◽"
+ },
+ "white_medium_square": {
+ "unicode": "25FB",
+ "unicode_alternates": ["25FB-FE0F"],
+ "name": "white medium square",
+ "shortname": ":white_medium_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "◻"
+ },
+ "white_small_square": {
+ "unicode": "25AB",
+ "unicode_alternates": ["25AB-FE0F"],
+ "name": "white small square",
+ "shortname": ":white_small_square:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "▫"
+ },
+ "white_square_button": {
+ "unicode": "1F533",
+ "unicode_alternates": [],
+ "name": "white square button",
+ "shortname": ":white_square_button:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["shape"],
+ "moji": "🔳"
+ },
+ "wind_blowing_face": {
+ "unicode": "1F32C",
+ "unicode_alternates": [],
+ "name": "wind blowing face",
+ "shortname": ":wind_blowing_face:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["mother", "nature"]
+ },
+ "wind_chime": {
+ "unicode": "1F390",
+ "unicode_alternates": [],
+ "name": "wind chime",
+ "shortname": ":wind_chime:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["ding", "nature", "wind", "chime", "bell", "fūrin", "instrument", "music", "spirits", "soothing", "protective", "spiritual", "sound"],
+ "moji": "🎐"
+ },
+ "wine_glass": {
+ "unicode": "1F377",
+ "unicode_alternates": [],
+ "name": "wine glass",
+ "shortname": ":wine_glass:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["alcohol", "beverage", "booze", "bottle", "drink", "drunk", "fermented", "glass", "grapes", "tasting", "wine", "winery"],
+ "moji": "🍷"
+ },
+ "wink": {
+ "unicode": "1F609",
+ "unicode_alternates": [],
+ "name": "winking face",
+ "shortname": ":wink:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [";)", ";-)", "*-)", "*)", ";-]", ";]", ";D", ";^)"],
+ "keywords": ["face", "happy", "mischievous", "secret", "wink", "winking", "friendly", "joke"],
+ "moji": "😉"
+ },
+ "wolf": {
+ "unicode": "1F43A",
+ "unicode_alternates": [],
+ "name": "wolf face",
+ "shortname": ":wolf:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["animal", "nature"],
+ "moji": "🐺"
+ },
+ "woman": {
+ "unicode": "1F469",
+ "unicode_alternates": [],
+ "name": "woman",
+ "shortname": ":woman:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["female", "girls"],
+ "moji": "👩"
+ },
+ "womans_clothes": {
+ "unicode": "1F45A",
+ "unicode_alternates": [],
+ "name": "womans clothes",
+ "shortname": ":womans_clothes:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["fashion", "woman", "clothing", "clothes", "blouse", "shirt", "wardrobe", "breasts", "cleavage", "shopping", "shop", "dressing", "dressed"],
+ "moji": "👚"
+ },
+ "womans_hat": {
+ "unicode": "1F452",
+ "unicode_alternates": [],
+ "name": "womans hat",
+ "shortname": ":womans_hat:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["accessories", "fashion", "female"],
+ "moji": "👒"
+ },
+ "womens": {
+ "unicode": "1F6BA",
+ "unicode_alternates": [],
+ "name": "womens symbol",
+ "shortname": ":womens:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["purple-square", "woman", "bathroom", "restroom", "sign", "girl", "female", "avatar"],
+ "moji": "🚺"
+ },
+ "worried": {
+ "unicode": "1F61F",
+ "unicode_alternates": [],
+ "name": "worried face",
+ "shortname": ":worried:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["concern", "face", "nervous", "worried", "anxious", "distressed", "nervous", "tense"],
+ "moji": "😟"
+ },
+ "wrench": {
+ "unicode": "1F527",
+ "unicode_alternates": [],
+ "name": "wrench",
+ "shortname": ":wrench:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["diy", "ikea", "tools"],
+ "moji": "🔧"
+ },
+ "writing_hand": {
+ "unicode": "1F58E",
+ "unicode_alternates": [],
+ "name": "left writing hand",
+ "shortname": ":writing_hand:",
+ "category": "people",
+ "aliases": [":left_writing_hand:"],
+ "aliases_ascii": [],
+ "keywords": ["write", "sign", "signature", "draw"]
+ },
+ "x": {
+ "unicode": "274C",
+ "unicode_alternates": [],
+ "name": "cross mark",
+ "shortname": ":x:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["delete", "no", "remove"],
+ "moji": "❌"
+ },
+ "yellow_heart": {
+ "unicode": "1F49B",
+ "unicode_alternates": [],
+ "name": "yellow heart",
+ "shortname": ":yellow_heart:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["affection", "like", "love", "valentines", "yellow", "gold", "heart", "love", "friendship", "happy", "happiness", "trust", "compassionate", "respectful", "honest", "caring", "selfless"],
+ "moji": "💛"
+ },
+ "yen": {
+ "unicode": "1F4B4",
+ "unicode_alternates": [],
+ "name": "banknote with yen sign",
+ "shortname": ":yen:",
+ "category": "objects",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["currency", "dollar", "japanese", "money", "yen", "japan", "japanese", "banknote", "money", "currency", "paper", "cash", "bill"],
+ "moji": "💴"
+ },
+ "yum": {
+ "unicode": "1F60B",
+ "unicode_alternates": [],
+ "name": "face savouring delicious food",
+ "shortname": ":yum:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["face", "happy", "joy", "smile", "tongue", "delicious", "savoring", "food", "eat", "yummy", "yum", "tasty", "savory"],
+ "moji": "😋"
+ },
+ "zap": {
+ "unicode": "26A1",
+ "unicode_alternates": ["26A1-FE0F"],
+ "name": "high voltage sign",
+ "shortname": ":zap:",
+ "category": "nature",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["lightning bolt", "thunder", "weather"],
+ "moji": "⚡"
+ },
+ "zero": {
+ "moji": "0️⃣",
+ "unicode": "0030-20E3",
+ "unicode_alternates": ["0030-FE0F-20E3"],
+ "name": "digit zero",
+ "shortname": ":zero:",
+ "category": "other",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["blue-square", "null", "numbers"]
+ },
+ "zzz": {
+ "unicode": "1F4A4",
+ "unicode_alternates": [],
+ "name": "sleeping symbol",
+ "shortname": ":zzz:",
+ "category": "emoticons",
+ "aliases": [],
+ "aliases_ascii": [],
+ "keywords": ["sleepy", "tired"],
+ "moji": "💤"
+ }
+}
diff --git a/lib/api/api.rb b/lib/api/api.rb
index afc0402f9e1..7834262d612 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -25,7 +25,7 @@ module API
format :json
content_type :txt, "text/plain"
- helpers APIHelpers
+ helpers Helpers
mount Groups
mount GroupMembers
@@ -52,5 +52,7 @@ module API
mount Labels
mount Settings
mount Keys
+ mount Tags
+ mount Triggers
end
end
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 2c0596c9dfb..1162271f5fc 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -53,7 +53,7 @@ module API
name = params[:name] || params[:context]
status = GenericCommitStatus.running_or_pending.find_by(commit: ci_commit, name: name, ref: params[:ref])
- status ||= GenericCommitStatus.new(commit: ci_commit, user: current_user)
+ status ||= GenericCommitStatus.new(project: @project, commit: ci_commit, user: current_user)
status.update(attrs)
case params[:state].to_s
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 883a5e14b17..26e7c956e8f 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -45,7 +45,8 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
- expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification
+ expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
+ expose :enable_ssl_verification
end
class ForkedFromProject < Grape::Entity
@@ -62,12 +63,14 @@ 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, :snippets_enabled, :created_at, :last_activity_at
+ expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :created_at, :last_activity_at
+ expose :shared_runners_enabled
expose :creator_id
expose :namespace
- expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? }
+ expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ |project, options| project.forked? }
expose :avatar_url
expose :star_count, :forks_count
+ expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? }
end
class ProjectMember < UserBasic
@@ -95,25 +98,6 @@ module API
end
end
- class RepoTag < Grape::Entity
- expose :name
- expose :message do |repo_obj, _options|
- if repo_obj.respond_to?(:message)
- repo_obj.message
- else
- nil
- end
- end
-
- expose :commit do |repo_obj, options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit
- elsif options[:project]
- options[:project].repository.commit(repo_obj.target)
- end
- end
- end
-
class RepoObject < Grape::Entity
expose :name
@@ -181,13 +165,15 @@ module API
end
class MergeRequest < ProjectEntity
- expose :target_branch, :source_branch, :upvotes, :downvotes
+ expose :target_branch, :source_branch
+ expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
expose :label_names, as: :labels
expose :description
expose :work_in_progress?, as: :work_in_progress
expose :milestone, using: Entities::Milestone
+ expose :merge_when_build_succeeds
end
class MergeRequestChanges < MergeRequest
@@ -211,6 +197,8 @@ module API
expose :author, using: Entities::UserBasic
expose :created_at
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
end
@@ -231,7 +219,7 @@ module API
class CommitStatus < Grape::Entity
expose :id, :sha, :ref, :status, :name, :target_url, :description,
- :created_at, :started_at, :finished_at
+ :created_at, :started_at, :finished_at, :allow_failure
expose :author, using: Entities::UserBasic
end
@@ -240,6 +228,8 @@ module API
expose :target_id, :target_type, :author_id
expose :data, :target_title
expose :created_at
+ expose :note, using: Entities::Note, if: ->(event, options) { event.note? }
+ expose :author, using: Entities::UserBasic, if: ->(event, options) { event.author }
expose :author_username do |event, options|
if event.author
@@ -264,7 +254,7 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events
+ expose :push_events, :issues_events, :merge_requests_events, :tag_push_events, :note_events, :build_events
# Expose serialized properties
expose :properties do |service, options|
field_names = service.fields.
@@ -341,5 +331,39 @@ module API
expose :user_oauth_applications
expose :after_sign_out_path
end
+
+ class Release < Grape::Entity
+ expose :tag, as: :tag_name
+ expose :description
+ end
+
+ class RepoTag < Grape::Entity
+ expose :name
+ expose :message do |repo_obj, _options|
+ if repo_obj.respond_to?(:message)
+ repo_obj.message
+ else
+ nil
+ end
+ end
+
+ expose :commit do |repo_obj, options|
+ if repo_obj.respond_to?(:commit)
+ repo_obj.commit
+ elsif options[:project]
+ options[:project].repository.commit(repo_obj.target)
+ end
+ end
+
+ expose :release, using: Entities::Release do |repo_obj, options|
+ if options[:project]
+ options[:project].releases.find_by(tag: repo_obj.name)
+ end
+ end
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ end
end
end
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 308c84dd135..8ad2c1883c7 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -7,7 +7,7 @@ module API
def commit_params(attrs)
{
file_path: attrs[:file_path],
- current_branch: attrs[:branch_name],
+ source_branch: attrs[:branch_name],
target_branch: attrs[:branch_name],
commit_message: attrs[:commit_message],
file_content: attrs[:content],
@@ -43,7 +43,8 @@ module API
# "content": "IyA9PSBTY2hlbWEgSW5mb3...",
# "ref": "master",
# "blob_id": "79f7bbd25901e8334750839545a9bd021f0e4c83",
- # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50"
+ # "commit_id": "d5a3ff139356ce33e37e73add446f16869741b50",
+ # "last_commit_id": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
# }
#
get ":id/repository/files" do
@@ -71,6 +72,7 @@ module API
ref: ref,
blob_id: blob.id,
commit_id: commit.id,
+ last_commit_id: user_project.repository.last_commit_for_path(commit.sha, file_path).id
}
else
not_found! 'File'
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 024aeec2e14..1a14d870a4a 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -65,6 +65,18 @@ module API
DestroyGroupService.new(group, current_user).execute
end
+ # Get a list of projects in this group
+ #
+ # Example Request:
+ # GET /groups/:id/projects
+ get ":id/projects" do
+ group = find_group(params[:id])
+ projects = group.projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::Project
+ end
+
# Transfer a project to the Group namespace
#
# Parameters:
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 549b1f9e9a7..a4df810e755 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,5 +1,5 @@
module API
- module APIHelpers
+ module Helpers
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER ="HTTP_SUDO"
@@ -133,6 +133,12 @@ module API
authorize! :admin_project, user_project
end
+ def require_gitlab_workhorse!
+ unless env['HTTP_GITLAB_WORKHORSE'].present?
+ forbidden!('Request should be executed via GitLab Workhorse')
+ end
+ end
+
def can?(object, action, subject)
abilities.allowed?(object, action, subject)
end
@@ -234,6 +240,10 @@ module API
render_api_error!(message || '409 Conflict', 409)
end
+ def file_to_large!
+ render_api_error!('413 Request Entity Too Large', 413)
+ end
+
def render_validation_error!(model)
if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
@@ -256,12 +266,7 @@ module API
projects = projects.search(params[:search])
end
- if params[:ci_enabled_first].present?
- projects.includes(:gitlab_ci_service).
- reorder("services.active DESC, projects.#{project_order_by} #{project_sort}")
- else
- projects.reorder(project_order_by => project_sort)
- end
+ projects.reorder(project_order_by => project_sort)
end
def project_order_by
@@ -282,6 +287,44 @@ module API
end
end
+ # file helpers
+
+ def uploaded_file!(field, uploads_path)
+ if params[field]
+ bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
+ return params[field]
+ end
+
+ # sanitize file paths
+ # this requires all paths to exist
+ required_attributes! %W(#{field}.path)
+ uploads_path = File.realpath(uploads_path)
+ file_path = File.realpath(params["#{field}.path"])
+ bad_request!('Bad file path') unless file_path.start_with?(uploads_path)
+
+ UploadedFile.new(
+ file_path,
+ params["#{field}.name"],
+ params["#{field}.type"] || 'application/octet-stream',
+ )
+ end
+
+ def present_file!(path, filename, content_type = 'application/octet-stream')
+ filename ||= File.basename(path)
+ header['Content-Disposition'] = "attachment; filename=#{filename}"
+ header['Content-Transfer-Encoding'] = 'binary'
+ content_type content_type
+
+ # Support download acceleration
+ case headers['X-Sendfile-Type']
+ when 'X-Sendfile'
+ header['X-Sendfile'] = path
+ body
+ else
+ file FileStreamer.new(path)
+ end
+ end
+
private
def add_pagination_headers(paginated, per_page)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 6eb84baf9cb..3c1c6bda260 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -76,6 +76,22 @@ module API
present merge_request, with: Entities::MergeRequest
end
+ # Show MR commits
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - The ID of MR
+ #
+ # Example:
+ # GET /projects/:id/merge_request/:merge_request_id/commits
+ #
+ get ':id/merge_request/:merge_request_id/commits' do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+ authorize! :read_merge_request, merge_request
+ present merge_request.commits, with: Entities::RepoCommit
+ end
+
# Show MR changes
#
# Parameters:
@@ -179,46 +195,54 @@ module API
# Merge MR
#
# Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
- # merge_commit_message (optional) - Custom merge commit message
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ # merge_commit_message (optional) - Custom merge commit message
+ # should_remove_source_branch (optional) - When true, the source branch will be deleted if possible
+ # merge_when_build_succeeds (optional) - When true, this MR will be merged when the build succeeds
# Example:
# PUT /projects/:id/merge_request/:merge_request_id/merge
#
put ":id/merge_request/:merge_request_id/merge" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
- allowed = ::Gitlab::GitAccess.new(current_user, user_project).
- can_push_to_branch?(merge_request.target_branch)
+ # 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?
- if allowed
- if merge_request.unchecked?
- merge_request.check_if_can_be_merged
- end
+ merge_request.check_if_can_be_merged if merge_request.unchecked?
- if merge_request.open? && !merge_request.work_in_progress?
- if merge_request.can_be_merged?
- commit_message = params[:merge_commit_message] || merge_request.merge_commit_message
-
- ::MergeRequests::MergeService.new(merge_request.target_project, current_user).
- execute(merge_request, commit_message)
-
- present merge_request, with: Entities::MergeRequest
- else
- render_api_error!('Branch cannot be merged', 405)
- end
- else
- # Merge request can not be merged
- # because it is already closed/merged or marked as WIP
- not_allowed!
- end
+ render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
+
+ merge_params = {
+ commit_message: params[:merge_commit_message],
+ should_remove_source_branch: params[:should_remove_source_branch]
+ }
+
+ if parse_boolean(params[:merge_when_build_succeeds]) && merge_request.ci_commit && merge_request.ci_commit.active?
+ ::MergeRequests::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user, merge_params).
+ execute(merge_request)
else
- # Merge request can not be merged
- # because user dont have permissions to push into target branch
- unauthorized!
+ ::MergeRequests::MergeService.new(merge_request.target_project, current_user, merge_params).
+ execute(merge_request)
end
+
+ present merge_request, with: Entities::MergeRequest
end
+ # Cancel Merge if Merge When build succeeds is enabled
+ # Parameters:
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
+ #
+ post ":id/merge_request/:merge_request_id/cancel_merge_when_build_succeeds" do
+ merge_request = user_project.merge_requests.find(params[:merge_request_id])
+
+ unauthorized! unless merge_request.can_cancel_merge_when_build_succeeds?(current_user)
+
+ ::MergeRequest::MergeWhenBuildSucceedsService.new(merge_request.target_project, current_user).cancel(merge_request)
+ end
# Get a merge request's comments
#
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 882d1a083ad..cf9938d25a7 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -45,6 +45,7 @@ module API
:merge_requests_events,
:tag_push_events,
:note_events,
+ :build_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
@@ -77,6 +78,7 @@ module API
:merge_requests_events,
:tag_push_events,
:note_events,
+ :build_events,
:enable_ssl_verification
]
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 67ee66a2058..0781236cf6d 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,12 +3,16 @@ module API
class Projects < Grape::API
before { authenticate! }
- resource :projects do
+ resource :projects, requirements: { id: /[^\/]+/ } do
helpers do
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
- publik = parse_boolean(publik)
- attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true
+ if publik.present? && !attrs[:visibility_level].present?
+ publik = parse_boolean(publik)
+ # Since setting the public attribute to private could mean either
+ # private or internal, use the more conservative option, private.
+ attrs[:visibility_level] = (publik == true) ? Gitlab::VisibilityLevel::PUBLIC : Gitlab::VisibilityLevel::PRIVATE
+ end
attrs
end
end
@@ -21,7 +25,7 @@ module API
@projects = current_user.authorized_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
- present @projects, with: Entities::Project
+ present @projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get an owned projects list for authenticated user
@@ -32,6 +36,17 @@ module API
@projects = current_user.owned_projects
@projects = filter_projects(@projects)
@projects = paginate @projects
+ present @projects, with: Entities::ProjectWithAccess, user: current_user
+ end
+
+ # Gets starred project for the authenticated user
+ #
+ # Example Request:
+ # GET /projects/starred
+ get '/starred' do
+ @projects = current_user.starred_projects
+ @projects = filter_projects(@projects)
+ @projects = paginate @projects
present @projects, with: Entities::Project
end
@@ -44,7 +59,7 @@ module API
@projects = Project.all
@projects = filter_projects(@projects)
@projects = paginate @projects
- present @projects, with: Entities::Project
+ present @projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get a single project
@@ -75,8 +90,10 @@ module API
# description (optional) - short project description
# issues_enabled (optional)
# merge_requests_enabled (optional)
+ # builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
+ # shared_runners_enabled (optional)
# namespace_id (optional) - defaults to user namespace
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - 0 by default
@@ -90,8 +107,10 @@ module API
:description,
:issues_enabled,
:merge_requests_enabled,
+ :builds_enabled,
:wiki_enabled,
:snippets_enabled,
+ :shared_runners_enabled,
:namespace_id,
:public,
:visibility_level,
@@ -117,8 +136,10 @@ module API
# default_branch (optional) - 'master' by default
# issues_enabled (optional)
# merge_requests_enabled (optional)
+ # builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
+ # shared_runners_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional)
# import_url (optional)
@@ -132,8 +153,10 @@ module API
:default_branch,
:issues_enabled,
:merge_requests_enabled,
+ :builds_enabled,
:wiki_enabled,
:snippets_enabled,
+ :shared_runners_enabled,
:public,
:visibility_level,
:import_url]
@@ -172,8 +195,10 @@ module API
# description (optional) - short project description
# issues_enabled (optional)
# merge_requests_enabled (optional)
+ # builds_enabled (optional)
# wiki_enabled (optional)
# snippets_enabled (optional)
+ # shared_runners_enabled (optional)
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
# Example Request
@@ -185,8 +210,10 @@ module API
:default_branch,
:issues_enabled,
:merge_requests_enabled,
+ :builds_enabled,
:wiki_enabled,
:snippets_enabled,
+ :shared_runners_enabled,
:public,
:visibility_level]
attrs = map_public_to_visibility_level(attrs)
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 20d568cf462..d7c48639eba 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -16,41 +16,6 @@ module API
end
end
- # Get a project repository tags
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/tags
- get ":id/repository/tags" do
- present user_project.repo.tags.sort_by(&:name).reverse,
- with: Entities::RepoTag, project: user_project
- end
-
- # Create tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # ref (required) - Create tag from commit sha or branch
- # message (optional) - Specifying a message creates an annotated tag.
- # Example Request:
- # POST /projects/:id/repository/tags
- post ':id/repository/tags' do
- authorize_push_project
- message = params[:message] || nil
- result = CreateTagService.new(user_project, current_user).
- execute(params[:tag_name], params[:ref], message)
-
- if result[:status] == :success
- present result[:tag],
- with: Entities::RepoTag,
- project: user_project
- else
- render_api_error!(result[:message], 400)
- end
- end
-
# Get a project repository tree
#
# Parameters:
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
new file mode 100644
index 00000000000..47621f443e6
--- /dev/null
+++ b/lib/api/tags.rb
@@ -0,0 +1,86 @@
+module API
+ # Git Tags API
+ class Tags < Grape::API
+ before { authenticate! }
+ before { authorize! :download_code, user_project }
+
+ resource :projects do
+ # Get a project repository tags
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id/repository/tags
+ get ":id/repository/tags" do
+ present user_project.repo.tags.sort_by(&:name).reverse,
+ with: Entities::RepoTag, project: user_project
+ end
+
+ # Create tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # ref (required) - Create tag from commit sha or branch
+ # message (optional) - Specifying a message creates an annotated tag.
+ # Example Request:
+ # POST /projects/:id/repository/tags
+ post ':id/repository/tags' do
+ authorize_push_project
+ message = params[:message] || nil
+ result = CreateTagService.new(user_project, current_user).
+ execute(params[:tag_name], params[:ref], message, params[:release_description])
+
+ if result[:status] == :success
+ present result[:tag],
+ with: Entities::RepoTag,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ # Add release notes to tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # 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
+ authorize_push_project
+ required_attributes! [:description]
+ result = CreateReleaseService.new(user_project, current_user).
+ execute(params[:tag_name], params[:description])
+
+ if result[:status] == :success
+ present result[:release], with: Entities::Release
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+
+ # Updates a release notes of a tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # 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
+ authorize_push_project
+ required_attributes! [:description]
+ result = UpdateReleaseService.new(user_project, current_user).
+ execute(params[:tag_name], params[:description])
+
+ if result[:status] == :success
+ present result[:release], with: Entities::Release
+ else
+ render_api_error!(result[:message], result[:http_status])
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
new file mode 100644
index 00000000000..2781f1cf191
--- /dev/null
+++ b/lib/api/triggers.rb
@@ -0,0 +1,48 @@
+module API
+ # Triggers API
+ class Triggers < Grape::API
+ resource :projects do
+ # Trigger a GitLab project build
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # ref (required) - The name of project's branch or tag
+ # token (required) - The uniq token of trigger
+ # variables (optional) - The list of variables to be injected into build
+ # Example Request:
+ # POST /projects/:id/trigger/builds
+ post ":id/trigger/builds" do
+ required_attributes! [:ref, :token]
+
+ project = Project.find_with_namespace(params[:id]) || Project.find_by(id: params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables]
+ if variables
+ unless variables.is_a?(Hash)
+ render_api_error!('variables needs to be a hash', 400)
+ end
+
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # convert variables from Mash to Hash
+ variables = variables.to_h
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index a98d668e02d..0d7813428e2 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -8,11 +8,17 @@ module API
#
# Example Request:
# GET /users
+ # GET /users?search=Admin
+ # GET /users?username=root
get do
- @users = User.all
- @users = @users.active if params[:active].present?
- @users = @users.search(params[:search]) if params[:search].present?
- @users = paginate @users
+ if params[:username].present?
+ @users = User.where(username: params[:username])
+ else
+ @users = User.all
+ @users = @users.active if params[:active].present?
+ @users = @users.search(params[:search]) if params[:search].present?
+ @users = paginate @users
+ end
if current_user.is_admin?
present @users, with: Entities::UserFull
@@ -33,7 +39,7 @@ module API
if current_user.is_admin?
present @user, with: Entities::UserFull
else
- present @user, with: Entities::UserBasic
+ present @user, with: Entities::User
end
end
diff --git a/lib/award_emoji.rb b/lib/award_emoji.rb
new file mode 100644
index 00000000000..783fcfb61ad
--- /dev/null
+++ b/lib/award_emoji.rb
@@ -0,0 +1,51 @@
+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
+
+ def self.normilize_emoji_name(name)
+ aliases[name] || name
+ end
+
+ def self.emoji_by_category
+ unless @emoji_by_category
+ @emoji_by_category = {}
+
+ emojis.each do |emoji_name, data|
+ data["name"] = emoji_name
+
+ @emoji_by_category[data["category"]] ||= []
+ @emoji_by_category[data["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
+end
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
new file mode 100644
index 00000000000..51fa3867e67
--- /dev/null
+++ b/lib/backup/artifacts.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Artifacts < Files
+ def initialize
+ super('artifacts', ArtifactUploader.artifacts_path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
index 6f56f680bb9..635967f4bd4 100644
--- a/lib/backup/builds.rb
+++ b/lib/backup/builds.rb
@@ -1,34 +1,13 @@
-module Backup
- class Builds
- attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir
+require 'backup/files'
+module Backup
+ class Builds < Files
def initialize
- @app_builds_dir = Settings.gitlab_ci.builds_path
- @backup_dir = Gitlab.config.backup.path
- @backup_builds_dir = File.join(Gitlab.config.backup.path, 'builds')
- end
-
- # Copy builds from builds directory to backup/builds
- def dump
- FileUtils.rm_rf(backup_builds_dir)
- # Ensure the parent dir of backup_builds_dir exists
- FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create backup_builds_dir before us
- FileUtils.mkdir(backup_builds_dir, mode: 0700)
- FileUtils.cp_r(app_builds_dir, backup_dir)
- end
-
- def restore
- backup_existing_builds_dir
-
- FileUtils.cp_r(backup_builds_dir, app_builds_dir)
+ super('builds', Settings.gitlab_ci.builds_path)
end
- def backup_existing_builds_dir
- timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
- if File.exists?(app_builds_dir)
- FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
- end
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
end
end
end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index 959ac4b7868..67b2a64bd10 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -2,26 +2,26 @@ require 'yaml'
module Backup
class Database
- attr_reader :config, :db_dir
+ attr_reader :config, :db_file_name
def initialize
@config = YAML.load_file(File.join(Rails.root,'config','database.yml'))[Rails.env]
- @db_dir = File.join(Gitlab.config.backup.path, 'db')
+ @db_file_name = File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
end
def dump
- FileUtils.rm_rf(@db_dir)
- # Ensure the parent dir of @db_dir exists
- FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create @db_dir before us
- FileUtils.mkdir(@db_dir, mode: 0700)
+ FileUtils.mkdir_p(File.dirname(db_file_name))
+ FileUtils.rm_f(db_file_name)
+ compress_rd, compress_wr = IO.pipe
+ compress_pid = spawn(*%W(gzip -1 -c), in: compress_rd, out: [db_file_name, 'w', 0600])
+ compress_rd.close
- success = case config["adapter"]
+ dump_pid = case config["adapter"]
when /^mysql/ then
$progress.print "Dumping MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- system('mysqldump', *mysql_args, config['database'], out: db_file_name)
+ spawn('mysqldump', *mysql_args, config['database'], out: compress_wr)
when "postgresql" then
$progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
@@ -30,48 +30,42 @@ module Backup
pgsql_args << "-n"
pgsql_args << Gitlab.config.backup.pg_schema
end
- system('pg_dump', *pgsql_args, config['database'], out: db_file_name)
+ spawn('pg_dump', *pgsql_args, config['database'], out: compress_wr)
end
- report_success(success)
- abort 'Backup failed' unless success
+ compress_wr.close
+
+ success = [compress_pid, dump_pid].all? { |pid| Process.waitpid(pid); $?.success? }
- $progress.print 'Compressing database ... '
- success = system('gzip', db_file_name)
report_success(success)
- abort 'Backup failed: compress error' unless success
+ abort 'Backup failed' unless success
end
def restore
- $progress.print 'Decompressing database ... '
- success = system('gzip', '-d', db_file_name_gz)
- report_success(success)
- abort 'Restore failed: decompress error' unless success
+ decompress_rd, decompress_wr = IO.pipe
+ decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+ decompress_wr.close
- success = case config["adapter"]
+ restore_pid = case config["adapter"]
when /^mysql/ then
$progress.print "Restoring MySQL database #{config['database']} ... "
# Workaround warnings from MySQL 5.6 about passwords on cmd line
ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
- system('mysql', *mysql_args, config['database'], in: db_file_name)
+ spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
when "postgresql" then
$progress.print "Restoring PostgreSQL database #{config['database']} ... "
pg_env
- system('psql', config['database'], '-f', db_file_name)
+ spawn('psql', config['database'], in: decompress_rd)
end
+ decompress_rd.close
+
+ success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+
report_success(success)
abort 'Restore failed' unless success
end
protected
- def db_file_name
- File.join(db_dir, 'database.sql')
- end
-
- def db_file_name_gz
- File.join(db_dir, 'database.sql.gz')
- end
-
def mysql_args
args = {
'host' => '--host',
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
new file mode 100644
index 00000000000..654b4d1c896
--- /dev/null
+++ b/lib/backup/files.rb
@@ -0,0 +1,40 @@
+require 'open3'
+
+module Backup
+ class Files
+ attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir
+
+ def initialize(name, app_files_dir)
+ @name = name
+ @app_files_dir = File.realpath(app_files_dir)
+ @files_parent_dir = File.realpath(File.join(@app_files_dir, '..'))
+ @backup_tarball = File.join(Gitlab.config.backup.path, name + '.tar.gz')
+ end
+
+ # Copy files from public/files to backup/files
+ def dump
+ FileUtils.mkdir_p(Gitlab.config.backup.path)
+ FileUtils.rm_f(backup_tarball)
+ run_pipeline!([%W(tar -C #{app_files_dir} -cf - .), %W(gzip -c -1)], out: [backup_tarball, 'w', 0600])
+ end
+
+ def restore
+ backup_existing_files_dir
+ create_files_dir
+
+ run_pipeline!([%W(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+ end
+
+ def backup_existing_files_dir
+ timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
+ if File.exists?(app_files_dir)
+ FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
+ end
+ end
+
+ def run_pipeline!(cmd_list, options={})
+ status_list = Open3.pipeline(*cmd_list, options)
+ abort 'Backup failed' unless status_list.compact.all?(&:success?)
+ end
+ end
+end
diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb
new file mode 100644
index 00000000000..4153467fbee
--- /dev/null
+++ b/lib/backup/lfs.rb
@@ -0,0 +1,13 @@
+require 'backup/files'
+
+module Backup
+ class Lfs < Files
+ def initialize
+ super('lfs', Settings.lfs.storage_path)
+ end
+
+ def create_files_dir
+ Dir.mkdir(app_files_dir, 0700)
+ end
+ end
+end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 5c42f25f4a2..099062eeb8b 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -150,17 +150,15 @@ module Backup
private
def backup_contents
- folders_to_backup + ["backup_information.yml"]
+ folders_to_backup + archives_to_backup + ["backup_information.yml"]
end
- def folders_to_backup
- folders = %w{repositories db uploads builds}
-
- if ENV["SKIP"]
- return folders.reject{ |folder| ENV["SKIP"].include?(folder) }
- end
+ def archives_to_backup
+ %w{uploads builds artifacts lfs}.map{ |name| (name + ".tar.gz") unless skipped?(name) }.compact
+ end
- folders
+ def folders_to_backup
+ %w{repositories db}.reject{ |name| skipped?(name) }
end
def settings
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 4d70f7883dd..a82a7e1f7bf 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -35,7 +35,7 @@ module Backup
if wiki.repository.empty?
$progress.puts " [SKIPPED]".cyan
else
- cmd = %W(git --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
+ 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
@@ -67,7 +67,7 @@ module Backup
FileUtils.mkdir_p(path_to_repo(project))
cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
else
- cmd = %W(git init --bare #{path_to_repo(project)})
+ cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_repo(project)})
end
if system(*cmd, silent)
@@ -87,7 +87,7 @@ module Backup
# that was initialized with ProjectWiki.new() and then
# try to restore with 'git clone --bare'.
FileUtils.rm_rf(path_to_repo(wiki))
- cmd = %W(git clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
+ 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
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 1f9626644e6..9261f77f3c9 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -1,34 +1,14 @@
+require 'backup/files'
+
module Backup
- class Uploads
- attr_reader :app_uploads_dir, :backup_uploads_dir, :backup_dir
+ class Uploads < Files
def initialize
- @app_uploads_dir = File.realpath(Rails.root.join('public', 'uploads'))
- @backup_dir = Gitlab.config.backup.path
- @backup_uploads_dir = File.join(Gitlab.config.backup.path, 'uploads')
- end
-
- # Copy uploads from public/uploads to backup/uploads
- def dump
- FileUtils.rm_rf(backup_uploads_dir)
- # Ensure the parent dir of backup_uploads_dir exists
- FileUtils.mkdir_p(Gitlab.config.backup.path)
- # Fail if somebody raced to create backup_uploads_dir before us
- FileUtils.mkdir(backup_uploads_dir, mode: 0700)
- FileUtils.cp_r(app_uploads_dir, backup_dir)
- end
-
- def restore
- backup_existing_uploads_dir
-
- FileUtils.cp_r(backup_uploads_dir, app_uploads_dir)
+ super('uploads', Rails.root.join('public/uploads'))
end
- def backup_existing_uploads_dir
- timestamped_uploads_path = File.join(app_uploads_dir, '..', "uploads.#{Time.now.to_i}")
- if File.exists?(app_uploads_dir)
- FileUtils.mv(app_uploads_dir, File.expand_path(timestamped_uploads_path))
- end
+ def create_files_dir
+ Dir.mkdir(app_files_dir)
end
end
end
diff --git a/lib/banzai.rb b/lib/banzai.rb
new file mode 100644
index 00000000000..093382261ae
--- /dev/null
+++ b/lib/banzai.rb
@@ -0,0 +1,13 @@
+module Banzai
+ def self.render(text, context = {})
+ Renderer.render(text, context)
+ end
+
+ def self.render_result(text, context = {})
+ Renderer.render_result(text, context)
+ end
+
+ def self.post_process(html, context)
+ Renderer.post_process(html, context)
+ end
+end
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
new file mode 100644
index 00000000000..ba2866e1efa
--- /dev/null
+++ b/lib/banzai/cross_project_reference.rb
@@ -0,0 +1,22 @@
+require 'banzai'
+
+module Banzai
+ # Common methods for ReferenceFilters that support an optional cross-project
+ # reference.
+ module CrossProjectReference
+ # Given a cross-project reference string, get the Project record
+ #
+ # Defaults to value of `context[:project]` if:
+ # * No reference is given OR
+ # * Reference given doesn't exist
+ #
+ # ref - String reference.
+ #
+ # Returns a Project, or nil if the reference can't be found
+ def project_from_ref(ref)
+ return context[:project] unless ref
+
+ Project.find_with_namespace(ref)
+ end
+ end
+end
diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb
new file mode 100644
index 00000000000..fd4fe024252
--- /dev/null
+++ b/lib/banzai/filter.rb
@@ -0,0 +1,10 @@
+require 'active_support/core_ext/string/output_safety'
+require 'banzai'
+
+module Banzai
+ module Filter
+ def self.[](name)
+ const_get("#{name.to_s.camelize}Filter")
+ end
+ end
+end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
new file mode 100644
index 00000000000..230387c8383
--- /dev/null
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -0,0 +1,158 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # Issues, Merge Requests, Snippets, Commits and Commit Ranges share
+ # similar functionality in reference filtering.
+ class AbstractReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ def self.object_class
+ # Implement in child class
+ # Example: MergeRequest
+ end
+
+ def self.object_name
+ object_class.name.underscore
+ end
+
+ def self.object_sym
+ object_name.to_sym
+ end
+
+ def self.data_reference
+ "data-#{object_name.dasherize}"
+ end
+
+ # Public: Find references in text (like `!123` for merge requests)
+ #
+ # AnyReferenceFilter.references_in(text) do |match, id, project_ref, matches|
+ # object = find_object(project_ref, id)
+ # "<a href=...>#{object.to_reference}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer referenced object ID, an optional String
+ # of the external project reference, and all of the matchdata.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text, pattern = object_class.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[object_sym].to_i, $~[:project], $~
+ 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
+
+ def object_sym
+ self.class.object_sym
+ end
+
+ def references_in(*args, &block)
+ self.class.references_in(*args, &block)
+ end
+
+ def find_object(project, id)
+ # Implement in child class
+ # Example: project.merge_requests.find
+ end
+
+ def url_for_object(object, project)
+ # Implement in child class
+ # Example: project_merge_request_url
+ end
+
+ def call
+ # `#123`
+ replace_text_nodes_matching(object_class.reference_pattern) do |content|
+ object_link_filter(content, object_class.reference_pattern)
+ end
+
+ # `[Issue](#123)`, which is turned into
+ # `<a href="#123">Issue</a>`
+ replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
+ object_link_filter(link, object_class.reference_pattern, link_text: text)
+ end
+
+ # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
+ replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
+ object_link_filter(text, object_class.link_reference_pattern)
+ end
+
+ # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
+ replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
+ object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ end
+ end
+
+ # Replace references (like `!123` for merge requests) in text with links
+ # to the referenced object's details page.
+ #
+ # text - String text to replace references in.
+ # pattern - Reference pattern to match against.
+ # link_text - Original content of the link being replaced.
+ #
+ # Returns a String with references replaced with links. All links
+ # have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
+ def object_link_filter(text, pattern, link_text: nil)
+ references_in(text, pattern) do |match, id, project_ref, matches|
+ project = project_from_ref(project_ref)
+
+ if project && object = find_object(project, id)
+ title = object_link_title(object)
+ klass = reference_class(object_sym)
+
+ data = data_attribute(
+ original: link_text || match,
+ project: project.id,
+ object_sym => object.id
+ )
+
+ url = matches[:url] if matches.names.include?("url")
+ url ||= url_for_object(object, project)
+
+ text = link_text || object_link_text(object, matches)
+
+ %(<a href="#{url}" #{data}
+ title="#{escape_once(title)}"
+ class="#{klass}">#{escape_once(text)}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = []
+
+ if matches.names.include?("anchor") && matches[:anchor] && matches[:anchor] =~ /\A\#note_(\d+)\z/
+ extras << "comment #{$1}"
+ end
+
+ extras
+ end
+
+ def object_link_title(object)
+ "#{object_class.name.titleize}: #{object.title}"
+ end
+
+ def object_link_text(object, matches)
+ text = object.reference_link_text(context[:project])
+
+ extras = object_link_text_extras(object, matches)
+ text += " (#{extras.join(", ")})" if extras.any?
+
+ text
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index c37c3bc55bf..da4ee80c1b5 100644
--- a/lib/gitlab/markdown/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -1,9 +1,9 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
require 'uri'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML Filter for auto-linking URLs in HTML.
#
# Based on HTML::Pipeline::AutolinkFilter
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
new file mode 100644
index 00000000000..e67cd45ab9b
--- /dev/null
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -0,0 +1,58 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces commit range references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitRangeReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ CommitRange
+ end
+
+ def self.references_in(text, pattern = CommitRange.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit_range], $~[:project], $~
+ 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)
+ 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::Application.routes.url_helpers
+ h.namespace_project_compare_url(project.namespace, project,
+ range.to_param.merge(only_path: context[:only_path]))
+ end
+
+ def object_link_title(range)
+ range.reference_title
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
new file mode 100644
index 00000000000..9e57608b483
--- /dev/null
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -0,0 +1,63 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces commit references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Commit
+ end
+
+ def self.references_in(text, pattern = Commit.reference_pattern)
+ text.gsub(pattern) do |match|
+ yield match, $~[:commit], $~[:project], $~
+ 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)
+ 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::Application.routes.url_helpers
+ h.namespace_project_commit_url(project.namespace, project, commit,
+ only_path: context[:only_path])
+ end
+
+ def object_link_title(commit)
+ commit.link_title
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = super
+
+ path = matches[:path] if matches.names.include?("path")
+ if path == '/builds'
+ extras.unshift "builds"
+ end
+
+ extras
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index da10e4d3760..86838e1483c 100644
--- a/lib/gitlab/markdown/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,10 +1,10 @@
require 'action_controller'
-require 'gitlab/markdown'
+require 'banzai'
require 'gitlab_emoji'
require 'html/pipeline/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that replaces :emoji: with images.
#
# Based on HTML::Pipeline::EmojiFilter
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 8f86f13976a..6136e73c096 100644
--- a/lib/gitlab/markdown/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -1,7 +1,7 @@
-require 'gitlab/markdown'
+require 'banzai'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that replaces external issue tracker references with links.
# References are ignored if the project doesn't use an external issue
# tracker.
@@ -23,6 +23,18 @@ module Gitlab
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? || project.default_issues_tracker?
@@ -30,6 +42,10 @@ module Gitlab
replace_text_nodes_matching(ExternalIssue.reference_pattern) do |content|
issue_link_filter(content)
end
+
+ replace_link_nodes_with_href(ExternalIssue.reference_pattern) do |link, text|
+ issue_link_filter(link, link_text: text)
+ end
end
# Replace `JIRA-123` issue references in text with links to the referenced
@@ -39,19 +55,23 @@ module Gitlab
#
# Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text)
+ def issue_link_filter(text, link_text: nil)
project = context[:project]
- self.class.references_in(text) do |match, issue|
- url = url_for_issue(issue, project, only_path: context[:only_path])
+ self.class.references_in(text) do |match, id|
+ ExternalIssue.new(id, project)
- title = escape_once("Issue in #{project.external_issue_tracker.title}")
+ url = url_for_issue(id, project, only_path: context[:only_path])
+
+ title = "Issue in #{project.external_issue_tracker.title}"
klass = reference_class(:issue)
- data = data_attribute(project: project.id)
+ data = data_attribute(project: project.id, external_issue: id)
+
+ text = link_text || match
%(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
+ title="#{escape_once(title)}"
+ class="#{klass}">#{escape_once(text)}</a>)
end
end
diff --git a/lib/gitlab/markdown/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index 29e51b6ade6..ac87b9820af 100644
--- a/lib/gitlab/markdown/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -1,16 +1,16 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML Filter to add a `rel="nofollow"` attribute to external links
#
class ExternalLinkFilter < HTML::Pipeline::Filter
def call
doc.search('a').each do |node|
- next unless node.has_attribute?('href')
+ link = node.attr('href')
- link = node.attribute('href').value
+ next unless link
# Skip non-HTTP(S) links
next unless link.start_with?('http')
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
new file mode 100644
index 00000000000..51180cb901a
--- /dev/null
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -0,0 +1,23 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces issue references with links. References to
+ # issues that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class IssueReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Issue
+ end
+
+ def find_object(project, id)
+ project.get_issue(id)
+ end
+
+ def url_for_object(issue, project)
+ IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index 618acb7a578..a3a7a23c1e6 100644
--- a/lib/gitlab/markdown/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -1,7 +1,7 @@
-require 'gitlab/markdown'
+require 'banzai'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that replaces label references with links.
class LabelReferenceFilter < ReferenceFilter
# Public: Find label references in text
@@ -30,6 +30,10 @@ module Gitlab
replace_text_nodes_matching(Label.reference_pattern) do |content|
label_link_filter(content)
end
+
+ replace_link_nodes_with_href(Label.reference_pattern) do |link, text|
+ label_link_filter(link, link_text: text)
+ end
end
# Replace label references in text with links to the label specified.
@@ -38,7 +42,7 @@ module Gitlab
#
# Returns a String with label references replaced with links. All links
# have `gfm` and `gfm-label` class names attached for styling.
- def label_link_filter(text)
+ def label_link_filter(text, link_text: nil)
project = context[:project]
self.class.references_in(text) do |match, id, name|
@@ -47,10 +51,16 @@ module Gitlab
if label = project.labels.find_by(params)
url = url_for_label(project, label)
klass = reference_class(:label)
- data = data_attribute(project: project.id, label: label.id)
+ data = data_attribute(
+ original: link_text || match,
+ project: project.id,
+ label: label.id
+ )
+
+ text = link_text || render_colored_label(label)
%(<a href="#{url}" #{data}
- class="#{klass}">#{render_colored_label(label)}</a>)
+ class="#{klass}">#{escape_once(text)}</a>)
else
match
end
@@ -59,9 +69,8 @@ module Gitlab
def url_for_label(project, label)
h = Gitlab::Application.routes.url_helpers
- h.namespace_project_issues_path(project.namespace, project,
- label_name: label.name,
- only_path: context[:only_path])
+ h.namespace_project_issues_url( project.namespace, project, label_name: label.name,
+ only_path: context[:only_path])
end
def render_colored_label(label)
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
new file mode 100644
index 00000000000..d09cf41df39
--- /dev/null
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -0,0 +1,42 @@
+require 'banzai'
+require 'html/pipeline/filter'
+
+module Banzai
+ module Filter
+ class MarkdownFilter < HTML::Pipeline::TextFilter
+ def initialize(text, context = nil, result = nil)
+ super text, context, result
+ @text = @text.delete "\r"
+ end
+
+ def call
+ html = self.class.renderer.render(@text)
+ html.rstrip!
+ html
+ end
+
+ private
+
+ def self.redcarpet_options
+ # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
+ @redcarpet_options ||= {
+ fenced_code_blocks: true,
+ footnotes: true,
+ lax_spacing: true,
+ no_intra_emphasis: true,
+ space_after_headers: true,
+ strikethrough: true,
+ superscript: true,
+ tables: true
+ }.freeze
+ end
+
+ def self.renderer
+ @renderer ||= begin
+ renderer = Redcarpet::Render::HTML.new
+ Redcarpet::Markdown.new(renderer, redcarpet_options)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
new file mode 100644
index 00000000000..755b946a34b
--- /dev/null
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -0,0 +1,41 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces merge request references with links. References
+ # to merge requests that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class MergeRequestReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ MergeRequest
+ end
+
+ def find_object(project, id)
+ project.merge_requests.find_by(iid: id)
+ end
+
+ def url_for_object(mr, project)
+ h = Gitlab::Application.routes.url_helpers
+ h.namespace_project_merge_request_url(project.namespace, project, mr,
+ only_path: context[:only_path])
+ end
+
+ def object_link_text_extras(object, matches)
+ extras = super
+
+ path = matches[:path] if matches.names.include?("path")
+ case path
+ when '/diffs'
+ extras.unshift "diffs"
+ when '/commits'
+ extras.unshift "commits"
+ when '/builds'
+ extras.unshift "builds"
+ end
+
+ extras
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index a1f3a8a8ebf..66f77902319 100644
--- a/lib/gitlab/markdown/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -1,8 +1,8 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that removes references to records that the current user does
# not have permission to view.
#
@@ -10,9 +10,12 @@ module Gitlab
#
class RedactorFilter < HTML::Pipeline::Filter
def call
- doc.css('a.gfm').each do |node|
- unless user_can_reference?(node)
- node.replace(node.text)
+ 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
@@ -21,12 +24,12 @@ module Gitlab
private
- def user_can_reference?(node)
+ def user_can_see_reference?(node)
if node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
- reference_filter = reference_type.constantize
+ reference_filter = Banzai::Filter.const_get(reference_type)
- reference_filter.user_can_reference?(current_user, node, context)
+ reference_filter.user_can_see_reference?(current_user, node, context)
else
true
end
diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index a4c560f578c..7198a8b03e2 100644
--- a/lib/gitlab/markdown/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -1,9 +1,9 @@
require 'active_support/core_ext/string/output_safety'
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# Base class for GitLab Flavored Markdown reference filters.
#
# References within <pre>, <code>, <a>, and <style> elements are ignored.
@@ -12,24 +12,7 @@ module Gitlab
# :project (required) - Current project, ignored if reference is cross-project.
# :only_path - Generate path-only links.
class ReferenceFilter < HTML::Pipeline::Filter
- LazyReference = Struct.new(:klass, :ids) do
- 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
-
- def load
- self.klass.where(id: self.ids)
- end
- end
-
- def self.user_can_reference?(user, node, context)
+ 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)
@@ -41,6 +24,10 @@ module Gitlab
end
end
+ def self.user_can_reference?(user, node, context)
+ true
+ end
+
def self.referenced_by(node)
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
@@ -53,19 +40,19 @@ module Gitlab
# Examples:
#
# data_attribute(project: 1, issue: 2)
- # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
+ # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"1\" data-issue=\"2\""
#
# data_attribute(project: 3, merge_request: 4)
- # # => "data-reference-filter=\"Gitlab::Markdown::SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
+ # # => "data-reference-filter=\"SomeReferenceFilter\" data-project=\"3\" data-merge-request=\"4\""
#
# Returns a String
def data_attribute(attributes = {})
- attributes[:reference_filter] = self.class.name
- attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{value}") }.join(" ")
+ attributes[:reference_filter] = self.class.name.demodulize
+ attributes.map { |key, value| %Q(data-#{key.to_s.dasherize}="#{escape_once(value)}") }.join(" ")
end
def escape_once(html)
- ERB::Util.html_escape_once(html)
+ html.html_safe? ? html : ERB::Util.html_escape_once(html)
end
def ignore_parents
@@ -122,6 +109,80 @@ module Gitlab
doc
end
+ # Iterate through the document's link nodes, yielding the current node's
+ # content if:
+ #
+ # * The `project` context value is present AND
+ # * The node's content matches `pattern`
+ #
+ # pattern - Regex pattern against which to match the node's content
+ #
+ # Yields the current node's String contents. The result of the block will
+ # replace the node and update the current document.
+ #
+ # Returns the updated Nokogiri::HTML::DocumentFragment object.
+ def replace_link_nodes_with_text(pattern)
+ return doc if project.nil?
+
+ doc.xpath('descendant-or-self::a').each do |node|
+ klass = node.attr('class')
+ next if klass && klass.include?('gfm')
+
+ link = node.attr('href')
+ text = node.text
+
+ next unless link && text
+
+ link = URI.decode(link)
+ # Ignore ending punctionation like periods or commas
+ next unless link == text && text =~ /\A#{pattern}/
+
+ html = yield text
+
+ next if html == text
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Iterate through the document's link nodes, yielding the current node's
+ # content if:
+ #
+ # * The `project` context value is present AND
+ # * The node's HREF matches `pattern`
+ #
+ # pattern - Regex pattern against which to match the node's HREF
+ #
+ # Yields the current node's String HREF and String content.
+ # The result of the block will replace the node and update the current document.
+ #
+ # Returns the updated Nokogiri::HTML::DocumentFragment object.
+ def replace_link_nodes_with_href(pattern)
+ return doc if project.nil?
+
+ doc.xpath('descendant-or-self::a').each do |node|
+ klass = node.attr('class')
+ next if klass && klass.include?('gfm')
+
+ link = node.attr('href')
+ text = node.text
+
+ next unless link && text
+ link = URI.decode(link)
+ next unless link && link =~ /\A#{pattern}\z/
+
+ html = yield link, text
+
+ next if html == link
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
# Ensure that a :project key exists in context
#
# Note that while the key might exist, its value could be nil!
diff --git a/lib/gitlab/markdown/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
index 00f983675e6..bef04112919 100644
--- a/lib/gitlab/markdown/reference_gatherer_filter.rb
+++ b/lib/banzai/filter/reference_gatherer_filter.rb
@@ -1,8 +1,8 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that gathers all referenced records that the current user has
# permission to view.
#
@@ -16,11 +16,11 @@ module Gitlab
end
def call
- doc.css('a.gfm').each do |node|
+ Querying.css(doc, 'a.gfm').each do |node|
gather_references(node)
end
- load_lazy_references unless context[:load_lazy_references] == false
+ load_lazy_references unless ReferenceExtractor.lazy?
doc
end
@@ -31,11 +31,13 @@ module Gitlab
return unless node.has_attribute?('data-reference-filter')
reference_type = node.attr('data-reference-filter')
- reference_filter = reference_type.constantize
+ reference_filter = Banzai::Filter.const_get(reference_type)
return if context[:reference_filter] && reference_filter != context[:reference_filter]
- return unless reference_filter.user_can_reference?(current_user, node, context)
+ 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
@@ -47,17 +49,20 @@ module Gitlab
end
end
- # Will load all references of one type using one query.
def load_lazy_references
refs = result[:references]
refs.each do |type, values|
- refs[type] = ReferenceFilter::LazyReference.load(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/gitlab/markdown/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 6ee3d1ce039..66f166939e4 100644
--- a/lib/gitlab/markdown/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -1,9 +1,9 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
require 'uri'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that "fixes" relative links to files in a repository.
#
# Context options:
@@ -16,7 +16,7 @@ module Gitlab
def call
return doc unless linkable_files?
- doc.search('a').each do |el|
+ doc.search('a:not(.gfm)').each do |el|
process_link_attr el.attribute('href')
end
@@ -51,7 +51,7 @@ module Gitlab
relative_url_root,
context[:project].path_with_namespace,
path_type(file_path),
- ref || 'master', # assume that if no ref exists we can point to master
+ ref || context[:project].default_branch, # if no ref exists, point to the default branch
file_path
].compact.join('/').squeeze('/').chomp('/')
@@ -91,7 +91,7 @@ module Gitlab
parts = request_path.split('/')
parts.pop if path_type(request_path) != 'tree'
- while parts.length > 1 && path.start_with?('../')
+ while path.start_with?('../')
parts.pop
path.sub!('../', '')
end
diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index e368de7d848..d03e3ae4b3c 100644
--- a/lib/gitlab/markdown/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -1,9 +1,9 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# Sanitize HTML
#
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
@@ -11,7 +11,7 @@ module Gitlab
def whitelist
# Descriptions are more heavily sanitized, allowing only a few elements.
# See http://git.io/vkuAN
- if pipeline == :description
+ if context[:inline_sanitization]
whitelist = LIMITED
whitelist[:elements] -= %w(pre code img ol ul li)
else
@@ -25,10 +25,6 @@ module Gitlab
private
- def pipeline
- context[:pipeline] || :default
- end
-
def customized?(transformers)
transformers.last.source_location[0] == __FILE__
end
@@ -48,6 +44,12 @@ module Gitlab
# Allow span elements
whitelist[:elements].push('span')
+ # Allow any protocol in `a` elements...
+ whitelist[:protocols].delete('a')
+
+ # ...but then remove links with the `javascript` protocol
+ whitelist[:transformers].push(remove_javascript_links)
+
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(remove_rel)
@@ -57,6 +59,19 @@ module Gitlab
whitelist
end
+ def remove_javascript_links
+ lambda do |env|
+ node = env[:node]
+
+ return unless node.name == 'a'
+ return unless node.has_attribute?('href')
+
+ if node['href'].start_with?('javascript', ':javascript')
+ node.remove_attribute('href')
+ end
+ end
+ end
+
def remove_rel
lambda do |env|
if env[:node_name] == 'a'
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
new file mode 100644
index 00000000000..1ad5df96f85
--- /dev/null
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -0,0 +1,25 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces snippet references with links. References to
+ # snippets that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class SnippetReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Snippet
+ end
+
+ def find_object(project, id)
+ project.snippets.find_by(id: id)
+ end
+
+ def url_for_object(snippet, project)
+ h = Gitlab::Application.routes.url_helpers
+ h.namespace_project_snippet_url(project.namespace, project, snippet,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index 8597e02f0de..c889cc1e97c 100644
--- a/lib/gitlab/markdown/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -1,9 +1,9 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML Filter to highlight fenced code blocks
#
class SyntaxHighlightFilter < HTML::Pipeline::Filter
diff --git a/lib/gitlab/markdown/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index bbb3bf7fc8b..9b3e67206d5 100644
--- a/lib/gitlab/markdown/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -1,8 +1,8 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that adds an anchor child element to all Headers in a
# document, so that they can be linked to.
#
@@ -31,7 +31,7 @@ module Gitlab
id = text.downcase
id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
- id.gsub!(' ', '-') # replace spaces with dash
+ id.tr!(' ', '-') # replace spaces with dash
id.squeeze!('-') # replace multiple dashes with one
uniq = (headers[id] > 0) ? "-#{headers[id]}" : ''
diff --git a/lib/gitlab/markdown/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 2f133ae8500..bdf7c2ebdfc 100644
--- a/lib/gitlab/markdown/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -1,8 +1,8 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'task_list/filter'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# Work around a bug in the default TaskList::Filter that adds a `task-list`
# class to every list element, regardless of whether or not it contains a
# task list.
diff --git a/lib/gitlab/markdown/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index fbada73ab86..1a1d0aad8ca 100644
--- a/lib/gitlab/markdown/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -1,9 +1,9 @@
-require 'gitlab/markdown'
+require 'banzai'
require 'html/pipeline/filter'
require 'uri'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that "fixes" relative upload links to files.
# Context options:
# :project (required) - Current project
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 2a594e1662e..964ab60f614 100644
--- a/lib/gitlab/markdown/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -1,7 +1,7 @@
-require 'gitlab/markdown'
+require 'banzai'
-module Gitlab
- module Markdown
+module Banzai
+ module Filter
# HTML filter that replaces user or group references with links.
#
# A special `@all` reference is also supported.
@@ -39,7 +39,7 @@ module Gitlab
end
end
- def self.user_can_reference?(user, node, context)
+ 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)
@@ -48,10 +48,26 @@ module Gitlab
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
replace_text_nodes_matching(User.reference_pattern) do |content|
user_link_filter(content)
end
+
+ replace_link_nodes_with_href(User.reference_pattern) do |link, text|
+ user_link_filter(link, link_text: text)
+ end
end
# Replace `@user` user references in text with links to the referenced
@@ -61,12 +77,12 @@ module Gitlab
#
# Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text)
+ def user_link_filter(text, link_text: nil)
self.class.references_in(text) do |match, username|
if username == 'all'
- link_to_all
+ link_to_all(link_text: link_text)
elsif namespace = Namespace.find_by(path: username)
- link_to_namespace(namespace) || match
+ link_to_namespace(namespace, link_text: link_text) || match
else
match
end
@@ -83,39 +99,42 @@ module Gitlab
reference_class(:project_member)
end
- def link_to_all
+ def link_to_all(link_text: nil)
project = context[:project]
-
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
data = data_attribute(project: project.id)
+ text = link_text || User.reference_prefix + 'all'
- text = User.reference_prefix + 'all'
- %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
+ link_tag(url, data, text)
end
- def link_to_namespace(namespace)
+ def link_to_namespace(namespace, link_text: nil)
if namespace.is_a?(Group)
- link_to_group(namespace.path, namespace)
+ link_to_group(namespace.path, namespace, link_text: link_text)
else
- link_to_user(namespace.path, namespace)
+ link_to_user(namespace.path, namespace, link_text: link_text)
end
end
- def link_to_group(group, namespace)
+ def link_to_group(group, namespace, link_text: nil)
url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(group: namespace.id)
+ text = link_text || Group.reference_prefix + group
- text = Group.reference_prefix + group
- %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
+ link_tag(url, data, text)
end
- def link_to_user(user, namespace)
+ def link_to_user(user, namespace, link_text: nil)
url = urls.user_url(user, only_path: context[:only_path])
data = data_attribute(user: namespace.owner_id)
+ text = link_text || User.reference_prefix + user
+
+ link_tag(url, data, text)
+ end
- text = User.reference_prefix + user
- %(<a href="#{url}" #{data} class="#{link_class}">#{text}</a>)
+ def link_tag(url, data, text)
+ %(<a href="#{url}" #{data} class="#{link_class}">#{escape_once(text)}</a>)
end
end
end
diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb
new file mode 100644
index 00000000000..073ec5d9801
--- /dev/null
+++ b/lib/banzai/lazy_reference.rb
@@ -0,0 +1,27 @@
+require 'banzai'
+
+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/pipeline.rb b/lib/banzai/pipeline.rb
new file mode 100644
index 00000000000..4e017809d9d
--- /dev/null
+++ b/lib/banzai/pipeline.rb
@@ -0,0 +1,10 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ def self.[](name)
+ name ||= :full
+ const_get("#{name.to_s.camelize}Pipeline")
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb
new file mode 100644
index 00000000000..5e76a817be5
--- /dev/null
+++ b/lib/banzai/pipeline/asciidoc_pipeline.rb
@@ -0,0 +1,13 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class AsciidocPipeline < BasePipeline
+ def self.filters
+ [
+ Filter::RelativeLinkFilter
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb
new file mode 100644
index 00000000000..957f352aec5
--- /dev/null
+++ b/lib/banzai/pipeline/atom_pipeline.rb
@@ -0,0 +1,14 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class AtomPipeline < FullPipeline
+ def self.transform_context(context)
+ super(context).merge(
+ only_path: false,
+ xhtml: true
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb
new file mode 100644
index 00000000000..cd30009e5c0
--- /dev/null
+++ b/lib/banzai/pipeline/base_pipeline.rb
@@ -0,0 +1,30 @@
+require 'banzai'
+require 'html/pipeline'
+
+module Banzai
+ module Pipeline
+ class BasePipeline
+ def self.filters
+ []
+ end
+
+ def self.transform_context(context)
+ context
+ end
+
+ def self.html_pipeline
+ @html_pipeline ||= HTML::Pipeline.new(filters)
+ end
+
+ class << self
+ %i(call to_document to_html).each do |meth|
+ define_method(meth) do |text, context|
+ context = transform_context(context)
+
+ html_pipeline.send(meth, text, context)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/combined_pipeline.rb b/lib/banzai/pipeline/combined_pipeline.rb
new file mode 100644
index 00000000000..f3bf1809d18
--- /dev/null
+++ b/lib/banzai/pipeline/combined_pipeline.rb
@@ -0,0 +1,27 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ module CombinedPipeline
+ def self.new(*pipelines)
+ Class.new(BasePipeline) do
+ const_set :PIPELINES, pipelines
+
+ def self.pipelines
+ self::PIPELINES
+ end
+
+ def self.filters
+ pipelines.flat_map(&:filters)
+ end
+
+ def self.transform_context(context)
+ pipelines.reduce(context) do |context, pipeline|
+ pipeline.transform_context(context)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
new file mode 100644
index 00000000000..94c2cb165a5
--- /dev/null
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -0,0 +1,14 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class DescriptionPipeline < FullPipeline
+ def self.transform_context(context)
+ super(context).merge(
+ # SanitizationFilter
+ inline_sanitization: true
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb
new file mode 100644
index 00000000000..14356145a35
--- /dev/null
+++ b/lib/banzai/pipeline/email_pipeline.rb
@@ -0,0 +1,13 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class EmailPipeline < FullPipeline
+ def self.transform_context(context)
+ super(context).merge(
+ only_path: false
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/full_pipeline.rb b/lib/banzai/pipeline/full_pipeline.rb
new file mode 100644
index 00000000000..72395a5d50e
--- /dev/null
+++ b/lib/banzai/pipeline/full_pipeline.rb
@@ -0,0 +1,9 @@
+require 'banzai'
+
+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
new file mode 100644
index 00000000000..38750b55ec7
--- /dev/null
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -0,0 +1,41 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class GfmPipeline < BasePipeline
+ def self.filters
+ @filters ||= [
+ Filter::SyntaxHighlightFilter,
+ Filter::SanitizationFilter,
+
+ Filter::UploadLinkFilter,
+ Filter::EmojiFilter,
+ Filter::TableOfContentsFilter,
+ Filter::AutolinkFilter,
+ Filter::ExternalLinkFilter,
+
+ Filter::UserReferenceFilter,
+ Filter::IssueReferenceFilter,
+ Filter::ExternalIssueReferenceFilter,
+ Filter::MergeRequestReferenceFilter,
+ Filter::SnippetReferenceFilter,
+ Filter::CommitRangeReferenceFilter,
+ Filter::CommitReferenceFilter,
+ Filter::LabelReferenceFilter,
+
+ Filter::TaskListFilter
+ ]
+ end
+
+ def self.transform_context(context)
+ context.merge(
+ only_path: true,
+
+ # EmojiFilter
+ asset_host: Gitlab::Application.config.asset_host,
+ asset_root: Gitlab.config.gitlab.base_url
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/note_pipeline.rb b/lib/banzai/pipeline/note_pipeline.rb
new file mode 100644
index 00000000000..89335143852
--- /dev/null
+++ b/lib/banzai/pipeline/note_pipeline.rb
@@ -0,0 +1,14 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class NotePipeline < FullPipeline
+ def self.transform_context(context)
+ super(context).merge(
+ # TableOfContentsFilter
+ no_header_anchors: true
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/plain_markdown_pipeline.rb b/lib/banzai/pipeline/plain_markdown_pipeline.rb
new file mode 100644
index 00000000000..998fd75daa2
--- /dev/null
+++ b/lib/banzai/pipeline/plain_markdown_pipeline.rb
@@ -0,0 +1,13 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class PlainMarkdownPipeline < BasePipeline
+ def self.filters
+ [
+ Filter::MarkdownFilter
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
new file mode 100644
index 00000000000..148f24b6ce1
--- /dev/null
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -0,0 +1,20 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class PostProcessPipeline < BasePipeline
+ def self.filters
+ [
+ Filter::RelativeLinkFilter,
+ Filter::RedactorFilter
+ ]
+ end
+
+ def self.transform_context(context)
+ context.merge(
+ post_process: true
+ )
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb
new file mode 100644
index 00000000000..4f9bc9fcccc
--- /dev/null
+++ b/lib/banzai/pipeline/reference_extraction_pipeline.rb
@@ -0,0 +1,13 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class ReferenceExtractionPipeline < BasePipeline
+ def self.filters
+ [
+ Filter::ReferenceGathererFilter
+ ]
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
new file mode 100644
index 00000000000..6725c9039a9
--- /dev/null
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -0,0 +1,9 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class SingleLinePipeline < GfmPipeline
+
+ end
+ end
+end
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
new file mode 100644
index 00000000000..1e1b51e683e
--- /dev/null
+++ b/lib/banzai/querying.rb
@@ -0,0 +1,18 @@
+module Banzai
+ module Querying
+ # Searches a Nokogiri document using a CSS query, optionally optimizing it
+ # whenever possible.
+ #
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ #
+ # Returns a Nokogiri::XML::NodeSet.
+ def self.css(document, query)
+ # When using "a.foo" Nokogiri compiles this to "//a[...]" but
+ # "descendant::a[...]" is quite a bit faster and achieves the same result.
+ xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+
+ document.xpath(xpath)
+ end
+ end
+end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
new file mode 100644
index 00000000000..2c197d31898
--- /dev/null
+++ b/lib/banzai/reference_extractor.rb
@@ -0,0 +1,55 @@
+require 'banzai'
+
+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
+
+ def analyze(text, context = {})
+ @texts << Renderer.render(text, context)
+ end
+
+ def references(type, context = {})
+ filter = Banzai::Filter["#{type}_reference"]
+
+ context.merge!(
+ pipeline: :reference_extraction,
+
+ # ReferenceGathererFilter
+ reference_filter: filter
+ )
+
+ 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
+ end
+ end
+end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
new file mode 100644
index 00000000000..910e1c6994e
--- /dev/null
+++ b/lib/banzai/renderer.rb
@@ -0,0 +1,85 @@
+module Banzai
+ module Renderer
+ # Convert a Markdown String into an HTML-safe String of HTML
+ #
+ # Note that while the returned HTML will have been sanitized of dangerous
+ # HTML, it may post a risk of information leakage if it's not also passed
+ # through `post_process`.
+ #
+ # Also note that the returned String is always HTML, not XHTML. Views
+ # requiring XHTML, such as Atom feeds, need to call `post_process` on the
+ # result, providing the appropriate `pipeline` option.
+ #
+ # markdown - Markdown String
+ # context - Hash of context options passed to our HTML Pipeline
+ #
+ # Returns an HTML-safe String
+ def self.render(text, context = {})
+ cache_key = context.delete(:cache_key)
+ cache_key = full_cache_key(cache_key, context[:pipeline])
+
+ cacheless = cacheless_render(text, context)
+
+ if cache_key && ENV["DEBUG_BANZAI_CACHE"]
+ cached = Rails.cache.fetch(cache_key) { cacheless }
+
+ if cached != cacheless
+ Rails.logger.warn "Banzai cache mismatch"
+ Rails.logger.warn "Text: #{text.inspect}"
+ Rails.logger.warn "Context: #{context.inspect}"
+ Rails.logger.warn "Cache key: #{cache_key.inspect}"
+ Rails.logger.warn "Cacheless: #{cacheless.inspect}"
+ Rails.logger.warn "With cache: #{cached.inspect}"
+ end
+ end
+
+ cacheless
+ end
+
+ def self.render_result(text, context = {})
+ Pipeline[context[:pipeline]].call(text, context)
+ end
+
+ # Perform post-processing on an HTML String
+ #
+ # This method is used to perform state-dependent changes to a String of
+ # HTML, such as removing references that the current user doesn't have
+ # permission to make (`RedactorFilter`).
+ #
+ # html - String to process
+ # context - Hash of options to customize output
+ # :pipeline - Symbol pipeline type
+ # :project - Project
+ # :user - User object
+ #
+ # Returns an HTML-safe String
+ def self.post_process(html, context)
+ context = Pipeline[context[:pipeline]].transform_context(context)
+
+ pipeline = Pipeline[:post_process]
+ if context[:xhtml]
+ pipeline.to_document(html, context).to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
+ else
+ pipeline.to_html(html, context)
+ end.html_safe
+ end
+
+ private
+
+ def self.cacheless_render(text, context = {})
+ result = render_result(text, context)
+
+ output = result[:output]
+ if output.respond_to?(:to_html)
+ output.to_html
+ else
+ output.to_s
+ end
+ end
+
+ def self.full_cache_key(cache_key, pipeline_name)
+ return unless cache_key
+ ["banzai", *cache_key, pipeline_name || :full]
+ end
+ end
+end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 218d8c3adcc..5c347e432b4 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -26,12 +26,11 @@ module Ci
format :json
helpers Helpers
- helpers ::API::APIHelpers
+ helpers ::API::Helpers
+ helpers Gitlab::CurrentSettings
mount Builds
- mount Commits
mount Runners
- mount Projects
mount Triggers
end
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 83ca1e6481c..15faa6edd84 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -47,6 +47,108 @@ module Ci
build.drop
end
end
+
+ # Authorize artifacts uploading for build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # filesize (optional) - the size of uploaded file
+ # Example Request:
+ # POST /builds/:id/artifacts/authorize
+ post ":id/artifacts/authorize" do
+ require_gitlab_workhorse!
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ forbidden!('build is not running') unless build.running?
+
+ if params[:filesize]
+ file_size = params[:filesize].to_i
+ file_to_large! unless file_size < max_artifacts_size
+ end
+
+ status 200
+ { TempPath: ArtifactUploader.artifacts_upload_path }
+ end
+
+ # Upload artifacts to build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # file (required) - The uploaded file
+ # Parameters (accelerated by GitLab Workhorse):
+ # file.path - path to locally stored body (generated by Workhorse)
+ # file.name - real filename as send in Content-Disposition
+ # file.type - real content type as send in Content-Type
+ # Headers:
+ # BUILD-TOKEN (required) - The build authorization token, the same as token
+ # Body:
+ # The file content
+ #
+ # Example Request:
+ # POST /builds/:id/artifacts
+ post ":id/artifacts" do
+ require_gitlab_workhorse!
+ not_allowed! unless Gitlab.config.artifacts.enabled
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ forbidden!('build is not running') unless build.running?
+
+ file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
+ file_to_large! unless file.size < max_artifacts_size
+
+ if build.update_attributes(artifacts_file: file)
+ present build, with: Entities::Build
+ else
+ render_validation_error!(build)
+ end
+ end
+
+ # Download the artifacts file from build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # Headers:
+ # BUILD-TOKEN (required) - The build authorization token, the same as token
+ # Example Request:
+ # GET /builds/:id/artifacts
+ get ":id/artifacts" do
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ artifacts_file = build.artifacts_file
+
+ unless artifacts_file.file_storage?
+ return redirect_to build.artifacts_file.url
+ end
+
+ unless artifacts_file.exists?
+ not_found!
+ end
+
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ end
+
+ # Remove the artifacts file from build
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # token (required) - The build authorization token
+ # Headers:
+ # BUILD-TOKEN (required) - The build authorization token, the same as token
+ # Example Request:
+ # DELETE /builds/:id/artifacts
+ delete ":id/artifacts" do
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ build.remove_artifacts_file!
+ end
end
end
end
diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb
deleted file mode 100644
index a60769d8305..00000000000
--- a/lib/ci/api/commits.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-module Ci
- module API
- class Commits < Grape::API
- resource :commits do
- # Get list of commits per project
- #
- # Parameters:
- # project_id (required) - The ID of a project
- # project_token (requires) - Project token
- # page (optional)
- # per_page (optional) - items per request (default is 20)
- #
- get do
- required_attributes! [:project_id, :project_token]
- project = Ci::Project.find(params[:project_id])
- authenticate_project_token!(project)
-
- commits = project.commits.page(params[:page]).per(params[:per_page] || 20)
- present commits, with: Entities::CommitWithBuilds
- end
-
- # Create a commit
- #
- # Parameters:
- # project_id (required) - The ID of a project
- # project_token (requires) - Project token
- # data (required) - GitLab push data
- #
- # Sample GitLab push data:
- # {
- # "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
- # "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- # "ref": "refs/heads/master",
- # "commits": [
- # {
- # "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- # "message": "Update Catalan translation to e38cb41.",
- # "timestamp": "2011-12-12T14:27:31+02:00",
- # "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- # "author": {
- # "name": "Jordi Mallach",
- # "email": "jordi@softcatala.org",
- # }
- # }, .... more commits
- # ]
- # }
- #
- # Example Request:
- # POST /commits
- post do
- required_attributes! [:project_id, :data, :project_token]
- project = Ci::Project.find(params[:project_id])
- authenticate_project_token!(project)
- commit = Ci::CreateCommitService.new.execute(project, current_user, params[:data])
-
- if commit.persisted?
- present commit, with: Entities::CommitWithBuilds
- else
- errors = commit.errors.full_messages.join(", ")
- render_api_error!(errors, 400)
- end
- end
- end
- end
- end
-end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
index b80c0b8b273..e4ac0545ea2 100644
--- a/lib/ci/api/entities.rb
+++ b/lib/ci/api/entities.rb
@@ -11,10 +11,16 @@ module Ci
expose :builds
end
+ class ArtifactFile < Grape::Entity
+ expose :filename, :size
+ end
+
class Build < Grape::Entity
expose :id, :commands, :ref, :sha, :status, :project_id, :repo_url,
:before_sha, :allow_git_fetch, :project_name
+ expose :name, :token, :stage
+
expose :options do |model|
model.options
end
@@ -24,21 +30,13 @@ module Ci
end
expose :variables
+ expose :artifacts_file, using: ArtifactFile
end
class Runner < Grape::Entity
expose :id, :token
end
- class Project < Grape::Entity
- expose :id, :name, :token, :default_ref, :gitlab_url, :path,
- :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id
-
- expose :timeout do |model|
- model.timeout
- end
- end
-
class RunnerProject < Grape::Entity
expose :id, :project_id, :runner_id
end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index e602cda81d6..1c91204e98c 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -1,22 +1,31 @@
module Ci
module API
module Helpers
+ BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
+ BUILD_TOKEN_PARAM = :token
UPDATE_RUNNER_EVERY = 60
def authenticate_runners!
- forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN
+ forbidden! unless runner_registration_token_valid?
end
def authenticate_runner!
forbidden! unless current_runner
end
- def authenticate_project_token!(project)
- forbidden! unless project.valid_token?(params[:project_token])
+ def authenticate_build_token!(build)
+ token = (params[BUILD_TOKEN_PARAM] || env[BUILD_TOKEN_HEADER]).to_s
+ forbidden! unless token && build.valid_token?(token)
+ end
+
+ def runner_registration_token_valid?
+ params[:token] == current_application_settings.runners_registration_token
end
def update_runner_last_contact
- if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY
+ # Use a random threshold to prevent beating DB updates
+ contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
+ if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age
current_runner.update_attributes(contacted_at: Time.now)
end
end
@@ -30,6 +39,10 @@ module Ci
info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
current_runner.update(info)
end
+
+ def max_artifacts_size
+ current_application_settings.max_artifacts_size.megabytes.to_i
+ end
end
end
end
diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb
deleted file mode 100644
index d719ad9e8d5..00000000000
--- a/lib/ci/api/projects.rb
+++ /dev/null
@@ -1,195 +0,0 @@
-module Ci
- module API
- # Projects API
- class Projects < Grape::API
- before { authenticate! }
-
- resource :projects do
- # Register new webhook for project
- #
- # Parameters
- # project_id (required) - The ID of a project
- # web_hook (required) - WebHook URL
- # Example Request
- # POST /projects/:project_id/webhooks
- post ":project_id/webhooks" do
- required_attributes! [:web_hook]
-
- project = Ci::Project.find(params[:project_id])
-
- unauthorized! unless can?(current_user, :admin_project, project.gl_project)
-
- web_hook = project.web_hooks.new({ url: params[:web_hook] })
-
- if web_hook.save
- present web_hook, with: Entities::WebHook
- else
- errors = web_hook.errors.full_messages.join(", ")
- render_api_error!(errors, 400)
- end
- end
-
- # Retrieve all Gitlab CI projects that the user has access to
- #
- # Example Request:
- # GET /projects
- get do
- gitlab_projects = current_user.authorized_projects
- gitlab_projects = filter_projects(gitlab_projects)
- gitlab_projects = paginate gitlab_projects
-
- ids = gitlab_projects.map { |project| project.id }
-
- projects = Ci::Project.where("gitlab_id IN (?)", ids).load
- present projects, with: Entities::Project
- end
-
- # Retrieve all Gitlab CI projects that the user owns
- #
- # Example Request:
- # GET /projects/owned
- get "owned" do
- gitlab_projects = current_user.owned_projects
- gitlab_projects = filter_projects(gitlab_projects)
- gitlab_projects = paginate gitlab_projects
-
- ids = gitlab_projects.map { |project| project.id }
-
- projects = Ci::Project.where("gitlab_id IN (?)", ids).load
- present projects, with: Entities::Project
- end
-
- # Retrieve info for a Gitlab CI project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id
- get ":id" do
- project = Ci::Project.find(params[:id])
- unauthorized! unless can?(current_user, :read_project, project.gl_project)
-
- present project, with: Entities::Project
- end
-
- # Create Gitlab CI project using Gitlab project info
- #
- # Parameters:
- # gitlab_id (required) - The gitlab id of the project
- # default_ref - The branch to run against (defaults to `master`)
- # Example Request:
- # POST /projects
- post do
- required_attributes! [:gitlab_id]
-
- filtered_params = {
- gitlab_id: params[:gitlab_id],
- # we accept gitlab_url for backward compatibility for a while (added to 7.11)
- default_ref: params[:default_ref] || 'master'
- }
-
- project = Ci::Project.new(filtered_params)
- project.build_missing_services
-
- if project.save
- present project, with: Entities::Project
- else
- errors = project.errors.full_messages.join(", ")
- render_api_error!(errors, 400)
- end
- end
-
- # Update a Gitlab CI project
- #
- # Parameters:
- # id (required) - The ID of a project
- # default_ref - The branch to run against (defaults to `master`)
- # Example Request:
- # PUT /projects/:id
- put ":id" do
- project = Ci::Project.find(params[:id])
-
- unauthorized! unless can?(current_user, :admin_project, project.gl_project)
-
- attrs = attributes_for_keys [:default_ref]
-
- if project.update_attributes(attrs)
- present project, with: Entities::Project
- else
- errors = project.errors.full_messages.join(", ")
- render_api_error!(errors, 400)
- end
- end
-
- # Remove a Gitlab CI project
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # DELETE /projects/:id
- delete ":id" do
- project = Ci::Project.find(params[:id])
-
- unauthorized! unless can?(current_user, :admin_project, project.gl_project)
-
- project.destroy
- end
-
- # Link a Gitlab CI project to a runner
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # runner_id (required) - The ID of a runner
- # Example Request:
- # POST /projects/:id/runners/:runner_id
- post ":id/runners/:runner_id" do
- project = Ci::Project.find(params[:id])
- runner = Ci::Runner.find(params[:runner_id])
-
- unauthorized! unless can?(current_user, :admin_project, project.gl_project)
-
- options = {
- project_id: project.id,
- runner_id: runner.id
- }
-
- runner_project = Ci::RunnerProject.new(options)
-
- if runner_project.save
- present runner_project, with: Entities::RunnerProject
- else
- errors = project.errors.full_messages.join(", ")
- render_api_error!(errors, 400)
- end
- end
-
- # Remove a Gitlab CI project from a runner
- #
- # Parameters:
- # id (required) - The ID of a CI project
- # runner_id (required) - The ID of a runner
- # Example Request:
- # DELETE /projects/:id/runners/:runner_id
- delete ":id/runners/:runner_id" do
- project = Ci::Project.find(params[:id])
- runner = Ci::Runner.find(params[:runner_id])
-
- unauthorized! unless can?(current_user, :admin_project, project.gl_project)
-
- options = {
- project_id: project.id,
- runner_id: runner.id
- }
-
- runner_project = Ci::RunnerProject.find_by(options)
-
- if runner_project.present?
- runner_project.destroy
- else
- not_found!
- end
- end
- end
- end
- end
-end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index 1466fe4356e..bfc14fe7a6b 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -3,17 +3,6 @@ module Ci
# Runners API
class Runners < Grape::API
resource :runners do
- # Get list of all available runners
- #
- # Example Request:
- # GET /runners
- get do
- authenticate!
- runners = Ci::Runner.all
-
- present runners, with: Entities::Runner
- end
-
# Delete runner
# Parameters:
# token (required) - The unique token of runner
@@ -40,14 +29,14 @@ module Ci
required_attributes! [:token]
runner =
- if params[:token] == GitlabCi::REGISTRATION_TOKEN
+ 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
)
- elsif project = Ci::Project.find_by(token: params[:token])
+ elsif project = Project.find_by(runners_token: params[:token])
# Create a specific runner for project.
project.runners.create(
description: params[:description],
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
index 40907d6db54..63b42113513 100644
--- a/lib/ci/api/triggers.rb
+++ b/lib/ci/api/triggers.rb
@@ -14,7 +14,7 @@ module Ci
post ":id/refs/:ref/trigger" do
required_attributes! [:token]
- project = Ci::Project.find(params[:id])
+ project = Project.find_by(ci_id: params[:id].to_i)
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
index 915a4f526a6..d53bdcbd0f2 100644
--- a/lib/ci/charts.rb
+++ b/lib/ci/charts.rb
@@ -60,7 +60,8 @@ module Ci
class BuildTime < Chart
def collect
- commits = project.commits.joins(:builds).where("#{Ci::Build.table_name}.finished_at is NOT NULL AND #{Ci::Build.table_name}.started_at is NOT NULL").last(30)
+ commits = project.ci_commits.last(30)
+
commits.each do |commit|
@labels << commit.short_sha
@build_times << (commit.duration / 60)
diff --git a/lib/ci/current_settings.rb b/lib/ci/current_settings.rb
deleted file mode 100644
index fd78b024970..00000000000
--- a/lib/ci/current_settings.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module Ci
- module CurrentSettings
- def current_application_settings
- key = :ci_current_application_settings
-
- RequestStore.store[key] ||= begin
- if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('ci_application_settings')
- Ci::ApplicationSetting.current || Ci::ApplicationSetting.create_from_defaults
- else
- fake_application_settings
- end
- end
- end
-
- def fake_application_settings
- OpenStruct.new(
- all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'],
- add_pusher: Ci::Settings.gitlab_ci['add_pusher'],
- )
- end
- end
-end
diff --git a/lib/ci/git.rb b/lib/ci/git.rb
deleted file mode 100644
index 7acc3f38edb..00000000000
--- a/lib/ci/git.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Ci
- module Git
- BLANK_SHA = '0' * 40
- end
-end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 0da73e387e1..bcdfd38d292 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,13 +4,14 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
- ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables]
- ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when]
+ ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage, :when, :artifacts, :cache]
- attr_reader :before_script, :image, :services, :variables
+ attr_reader :before_script, :image, :services, :variables, :path, :cache
- def initialize(config)
- @config = YAML.load(config)
+ def initialize(config, path = nil)
+ @config = YAML.safe_load(config, [Symbol])
+ @path = path
unless @config.is_a? Hash
raise ValidationError, "YAML should be a hash"
@@ -45,6 +46,7 @@ module Ci
@services = @config[:services]
@stages = @config[:stages] || @config[:types]
@variables = @config[:variables] || {}
+ @cache = @config[:cache]
@config.except!(*ALLOWED_YAML_KEYS)
# anything that doesn't have script is considered as unknown
@@ -63,26 +65,6 @@ module Ci
end
end
- def process?(only_params, except_params, ref, tag)
- return true if only_params.nil? && except_params.nil?
-
- if only_params
- return true if tag && only_params.include?("tags")
- return true if !tag && only_params.include?("branches")
-
- only_params.find do |pattern|
- match_ref?(pattern, ref)
- end
- else
- return false if tag && except_params.include?("tags")
- return false if !tag && except_params.include?("branches")
-
- except_params.each do |pattern|
- return false if match_ref?(pattern, ref)
- end
- end
- end
-
def build_job(name, job)
{
stage_idx: stages.index(job[:stage]),
@@ -96,19 +78,13 @@ module Ci
when: job[:when] || 'on_success',
options: {
image: job[:image] || @image,
- services: job[:services] || @services
+ services: job[:services] || @services,
+ artifacts: job[:artifacts],
+ cache: job[:cache] || @cache,
}.compact
}
end
- def match_ref?(pattern, ref)
- if pattern.first == "/" && pattern.last == "/"
- Regexp.new(pattern[1...-1]) =~ ref
- else
- pattern == ref
- end
- end
-
def normalize_script(script)
if script.is_a? Array
script.join("\n")
@@ -138,67 +114,154 @@ module Ci
raise ValidationError, "variables should be a map of key-valued strings"
end
+ if @cache
+ 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
+
@jobs.each do |name, job|
- validate_job!("#{name} job", job)
+ validate_job!(name, job)
end
true
end
def validate_job!(name, job)
+ validate_job_name!(name)
+ validate_job_keys!(name, job)
+ validate_job_types!(name, job)
+
+ validate_job_stage!(name, job) if job[:stage]
+ validate_job_cache!(name, job) if job[:cache]
+ validate_job_artifacts!(name, job) if job[:artifacts]
+ end
+
+ private
+
+ def validate_job_name!(name)
+ if name.blank? || !validate_string(name)
+ raise ValidationError, "job name should be non-empty string"
+ end
+ end
+
+ def validate_job_keys!(name, job)
job.keys.each do |key|
unless ALLOWED_JOB_KEYS.include? key
- raise ValidationError, "#{name}: unknown parameter #{key}"
+ raise ValidationError, "#{name} job: unknown parameter #{key}"
end
end
+ end
- if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script])
- raise ValidationError, "#{name}: script should be a string or an array of a strings"
- end
-
- if job[:stage]
- unless job[:stage].is_a?(String) && job[:stage].in?(stages)
- raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}"
- end
+ def validate_job_types!(name, job)
+ if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name} job: script should be a string or an array of a strings"
end
- if job[:image] && !job[:image].is_a?(String)
- raise ValidationError, "#{name}: image should be a string"
+ if job[:image] && !validate_string(job[:image])
+ raise ValidationError, "#{name} job: image should be a string"
end
if job[:services] && !validate_array_of_strings(job[:services])
- raise ValidationError, "#{name}: services should be an array of strings"
+ raise ValidationError, "#{name} job: services should be an array of strings"
end
if job[:tags] && !validate_array_of_strings(job[:tags])
- raise ValidationError, "#{name}: tags parameter should be an array of strings"
+ 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}: only parameter should be an array of strings"
+ raise ValidationError, "#{name} job: only parameter should be an array of strings"
end
if job[:except] && !validate_array_of_strings(job[:except])
- raise ValidationError, "#{name}: except parameter should be an array of strings"
+ raise ValidationError, "#{name} job: except parameter should be an array of strings"
end
- if job[:allow_failure] && !job[:allow_failure].in?([true, false])
- raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
+ 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))
- raise ValidationError, "#{name}: when parameter should be on_success, on_failure or always"
+ raise ValidationError, "#{name} job: when parameter should be on_success, on_failure or always"
end
end
- private
+ 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(", ")}"
+ end
+ end
+
+ def validate_job_cache!(name, job)
+ if job[:cache][:untracked] && !validate_boolean(job[:cache][:untracked])
+ raise ValidationError, "#{name} job: cache:untracked parameter should be an boolean"
+ end
+
+ if job[:cache][:paths] && !validate_array_of_strings(job[:cache][:paths])
+ raise ValidationError, "#{name} job: cache:paths parameter should be an array of strings"
+ end
+ end
+
+ def validate_job_artifacts!(name, job)
+ if job[:artifacts][:untracked] && !validate_boolean(job[:artifacts][:untracked])
+ raise ValidationError, "#{name} job: artifacts:untracked parameter should be an boolean"
+ end
+
+ 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
+ end
def validate_array_of_strings(values)
- values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)}
+ values.is_a?(Array) && values.all? { |value| validate_string(value) }
end
def validate_variables(variables)
- variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)}
+ 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)
+ if only_params.present?
+ return false unless matching?(only_params, ref, tag)
+ end
+
+ if except_params.present?
+ return false if matching?(except_params, ref, tag)
+ end
+
+ true
+ end
+
+ def matching?(patterns, ref, tag)
+ patterns.any? do |pattern|
+ match_ref?(pattern, ref, tag)
+ end
+ end
+
+ def match_ref?(pattern, ref, tag)
+ pattern, path = pattern.split('@', 2)
+ return false if path && path != self.path
+ return true if tag && pattern == 'tags'
+ return true if !tag && pattern == 'branches'
+
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ ref
+ else
+ pattern == ref
+ end
end
end
end
diff --git a/lib/ci/scheduler.rb b/lib/ci/scheduler.rb
deleted file mode 100644
index ee0958f4be1..00000000000
--- a/lib/ci/scheduler.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module Ci
- class Scheduler
- def perform
- projects = Ci::Project.where(always_build: true).all
- projects.each do |project|
- last_commit = project.commits.last
- next unless last_commit && last_commit.last_build
-
- interval = project.polling_interval
- if (last_commit.last_build.created_at + interval.hours) < Time.now
- last_commit.retry
- end
- end
- end
- end
-end
diff --git a/lib/file_streamer.rb b/lib/file_streamer.rb
new file mode 100644
index 00000000000..4e3c6d3c773
--- /dev/null
+++ b/lib/file_streamer.rb
@@ -0,0 +1,16 @@
+class FileStreamer #:nodoc:
+ attr_reader :to_path
+
+ def initialize(path)
+ @to_path = path
+ end
+
+ # Stream the file's contents if Rack::Sendfile isn't present.
+ def each
+ File.open(to_path, 'rb') do |file|
+ while chunk = file.read(16384)
+ yield chunk
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index bf33e5b1b1e..b203b9d70e4 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,14 +1,10 @@
require 'asciidoctor'
-require 'html/pipeline'
module Gitlab
# Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
# the resulting HTML through HTML pipeline filters.
module Asciidoc
- # Provide autoload paths for filters to prevent a circular dependency error
- autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
-
DEFAULT_ADOC_ATTRS = [
'showtitle', 'idprefix=user-content-', 'idseparator=-', 'env=gitlab',
'env-gitlab', 'source-highlighter=html-pipeline'
@@ -24,13 +20,11 @@ module Gitlab
# :requested_path
# :ref
# asciidoc_opts - a Hash of options to pass to the Asciidoctor converter
- # html_opts - a Hash of options for HTML output:
- # :xhtml - output XHTML instead of HTML
#
- def self.render(input, context, asciidoc_opts = {}, html_opts = {})
- asciidoc_opts = asciidoc_opts.reverse_merge(
+ def self.render(input, context, asciidoc_opts = {})
+ asciidoc_opts.reverse_merge!(
safe: :secure,
- backend: html_opts[:xhtml] ? :xhtml5 : :html5,
+ backend: :html5,
attributes: []
)
asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
@@ -38,23 +32,10 @@ module Gitlab
html = ::Asciidoctor.convert(input, asciidoc_opts)
if context[:project]
- result = HTML::Pipeline.new(filters).call(html, context)
-
- save_opts = html_opts[:xhtml] ?
- Nokogiri::XML::Node::SaveOptions::AS_XHTML : 0
-
- html = result[:output].to_html(save_with: save_opts)
+ html = Banzai.render(html, context.merge(pipeline: :asciidoc))
end
html.html_safe
end
-
- private
-
- def self.filters
- [
- Gitlab::Markdown::RelativeLinkFilter
- ]
- end
end
end
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 6830a916bcb..cdcaae8094c 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -33,8 +33,11 @@ module Grack
auth!
+ lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
+ return lfs_response unless lfs_response.nil?
+
if project && authorized_request?
- # Tell gitlab-git-http-server the request is OK, and what the GL_ID is
+ # Tell gitlab-workhorse the request is OK, and what the GL_ID is
render_grack_auth_ok
elsif @user.nil? && !@ci
unauthorized
@@ -72,9 +75,11 @@ module Grack
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
if project && matched_login.present? && git_cmd == 'git-upload-pack'
- underscored_service = matched_login['s'].underscore
+ underscored_service = matched_login['s'].underscore
- if Service.available_services_names.include?(underscored_service)
+ if underscored_service == 'gitlab_ci'
+ return project && project.valid_build_token?(password)
+ elsif Service.available_services_names.include?(underscored_service)
service_method = "#{underscored_service}_service"
service = project.send(service_method)
@@ -193,12 +198,19 @@ module Grack
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' => project.repository.path_to_repo,
+ 'RepoPath' => repo_path,
})]
]
end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 59f7a45b791..4c15d58d680 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -1,8 +1,8 @@
module Gitlab
class Shell
- class AccessDenied < StandardError; end
+ class Error < StandardError; end
- class KeyAdder < Struct.new(:io)
+ KeyAdder = Struct.new(:io) do
def add_key(id, key)
key.gsub!(/[[:space:]]+/, ' ').strip!
io.puts("#{id}\t#{key}")
@@ -36,8 +36,9 @@ module Gitlab
# import_repository("gitlab/gitlab-ci", "https://github.com/randx/six.git")
#
def import_repository(name, url)
- Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'import-project',
- "#{name}.git", url, '240'])
+ output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', "#{name}.git", url, '240'])
+ raise Error, output unless status.zero?
+ true
end
# Move repository
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index 35e34d033e0..03aac1a025a 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -11,7 +11,8 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(current_user,
+ project = ::Projects::CreateService.new(
+ current_user,
name: repo["name"],
path: repo["slug"],
description: repo["description"],
diff --git a/lib/gitlab/blacklist.rb b/lib/gitlab/blacklist.rb
deleted file mode 100644
index 43145e0ee1b..00000000000
--- a/lib/gitlab/blacklist.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Gitlab
- module Blacklist
- extend self
-
- def path
- %w(
- admin
- dashboard
- files
- groups
- help
- profile
- projects
- search
- public
- assets
- u
- s
- teams
- merge_requests
- issues
- users
- snippets
- services
- repository
- hooks
- notes
- unsubscribes
- all
- ci
- )
- end
- end
-end
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb
new file mode 100644
index 00000000000..86bfa0a4378
--- /dev/null
+++ b/lib/gitlab/build_data_builder.rb
@@ -0,0 +1,64 @@
+module Gitlab
+ class BuildDataBuilder
+ class << self
+ def build(build)
+ project = build.project
+ commit = build.commit
+ user = build.user
+
+ data = {
+ object_kind: 'build',
+
+ ref: build.ref,
+ tag: build.tag,
+ before_sha: build.before_sha,
+ sha: build.sha,
+
+ # TODO: should this be not prefixed with build_?
+ # Leaving this way to have backward compatibility
+ build_id: build.id,
+ build_name: build.name,
+ build_stage: build.stage,
+ build_status: build.status,
+ build_started_at: build.started_at,
+ build_finished_at: build.finished_at,
+ build_duration: build.duration,
+
+ # TODO: do we still need it?
+ project_id: project.id,
+ project_name: project.name_with_namespace,
+
+ user: {
+ id: user.try(:id),
+ name: user.try(:name),
+ email: user.try(:email),
+ },
+
+ commit: {
+ id: commit.id,
+ sha: commit.sha,
+ message: commit.git_commit_message,
+ author_name: commit.git_author_name,
+ author_email: commit.git_author_email,
+ status: commit.status,
+ duration: commit.duration,
+ started_at: commit.started_at,
+ finished_at: commit.finished_at,
+ },
+
+ repository: {
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url,
+ git_http_url: project.http_url_to_repo,
+ git_ssh_url: project.ssh_url_to_repo,
+ visibility_level: project.visibility_level
+ },
+ }
+
+ data
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index aeec595782c..9bef9037ad6 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -1,6 +1,12 @@
module Gitlab
class ClosingIssueExtractor
- ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern)
+ ISSUE_CLOSING_REGEX = begin
+ link_pattern = URI.regexp(%w(http https))
+
+ pattern = Gitlab.config.gitlab.issue_closing_pattern
+ pattern = pattern.sub('%{issue_ref}', "(?:(?:#{link_pattern})|(?:#{Issue.reference_pattern}))")
+ Regexp.new(pattern).freeze
+ end
def initialize(project, current_user = nil)
@extractor = Gitlab::ReferenceExtractor.new(project, current_user)
@@ -9,10 +15,12 @@ module Gitlab
def closed_by_message(message)
return [] if message.nil?
- closing_statements = message.scan(ISSUE_CLOSING_REGEX).
- map { |ref| ref[0] }.join(" ")
+ closing_statements = []
+ message.scan(ISSUE_CLOSING_REGEX) do
+ closing_statements << Regexp.last_match[0]
+ end
- @extractor.analyze(closing_statements)
+ @extractor.analyze(closing_statements.join(" "))
@extractor.issues
end
diff --git a/lib/gitlab/compare_result.rb b/lib/gitlab/compare_result.rb
index d72391dade5..0d696a1ee28 100644
--- a/lib/gitlab/compare_result.rb
+++ b/lib/gitlab/compare_result.rb
@@ -2,8 +2,8 @@ module Gitlab
class CompareResult
attr_reader :commits, :diffs
- def initialize(compare)
- @commits, @diffs = compare.commits, compare.diffs
+ def initialize(compare, diff_options = {})
+ @commits, @diffs = compare.commits, compare.diffs(nil, diff_options)
end
end
end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 8a7f8dc5003..85583dce9ee 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -45,11 +45,11 @@ module Gitlab
end
def starting_year
- (Time.now - 1.year).strftime("%Y")
+ 1.year.ago.year
end
def starting_month
- Date.today.strftime("%m").to_i
+ Date.today.month
end
end
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 0ea1b6a2f6f..7a86c09158e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -23,7 +23,9 @@ module Gitlab
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
session_expire_delay: Settings.gitlab['session_expire_delay'],
- import_sources: Settings.gitlab['import_sources']
+ import_sources: Settings.gitlab['import_sources'],
+ shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
+ max_artifacts_size: Settings.artifacts['max_size'],
)
end
@@ -36,7 +38,9 @@ module Gitlab
true
end
- use_db && ActiveRecord::Base.connection.active? && ActiveRecord::Base.connection.table_exists?('application_settings')
+ use_db && ActiveRecord::Base.connection.active? &&
+ !ActiveRecord::Migrator.needs_migration? &&
+ ActiveRecord::Base.connection.table_exists?('application_settings')
end
end
end
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 741a52714ac..de77a6fbff1 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -1,11 +1,29 @@
module Gitlab
module Database
def self.mysql?
- ActiveRecord::Base.connection.adapter_name.downcase == 'mysql'
+ ActiveRecord::Base.connection.adapter_name.downcase == 'mysql2'
end
def self.postgresql?
ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
end
+
+ def true_value
+ case ActiveRecord::Base.connection.adapter_name.downcase
+ when 'postgresql'
+ "'t'"
+ else
+ 1
+ end
+ end
+
+ def false_value
+ case ActiveRecord::Base.connection.adapter_name.downcase
+ when 'postgresql'
+ "'f'"
+ else
+ 0
+ end
+ end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 142058aa69d..79061cd0141 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -46,11 +46,11 @@ module Gitlab
end
def added_lines
- diff_lines.select(&:added?).size
+ diff_lines.count(&:added?)
end
def removed_lines
- diff_lines.select(&:removed?).size
+ diff_lines.count(&:removed?)
end
end
end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
new file mode 100644
index 00000000000..a2eb7a70bd2
--- /dev/null
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -0,0 +1,137 @@
+module Gitlab
+ module Email
+ module Message
+ class RepositoryPush
+ attr_accessor :recipient
+ attr_reader :author_id, :ref, :action
+
+ include Gitlab::Application.routes.url_helpers
+
+ delegate :namespace, :name_with_namespace, to: :project, prefix: :project
+ delegate :name, to: :author, prefix: :author
+
+ def initialize(notify, project_id, recipient, 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)
+ @ref = @opts.delete(:ref)
+ @action = @opts.delete(:action)
+ end
+
+ def project
+ @project ||= Project.find(@project_id)
+ end
+
+ def author
+ @author ||= User.find(@author_id)
+ end
+
+ def commits
+ @commits ||= (Commit.decorate(compare.commits, project) if compare)
+ end
+
+ def diffs
+ @diffs ||= (compare.diffs if compare)
+ end
+
+ def diffs_count
+ diffs.count if diffs
+ end
+
+ def compare
+ @opts[:compare]
+ end
+
+ def compare_timeout
+ compare.timeout if compare
+ end
+
+ def reverse_compare?
+ @opts[:reverse_compare] || false
+ end
+
+ def disable_diffs?
+ @opts[:disable_diffs] || false
+ end
+
+ def send_from_committer_email?
+ @opts[:send_from_committer_email] || false
+ end
+
+ def action_name
+ @action_name ||=
+ case @action
+ when :create
+ 'pushed new'
+ when :delete
+ 'deleted'
+ else
+ 'pushed to'
+ end
+ end
+
+ def ref_name
+ @ref_name ||= Gitlab::Git.ref_name(@ref)
+ end
+
+ def ref_type
+ @ref_type ||= Gitlab::Git.tag_ref?(@ref) ? 'tag' : 'branch'
+ end
+
+ def target_url
+ if @action == :push && commits
+ if commits.length > 1
+ namespace_project_compare_url(project_namespace,
+ project,
+ from: Commit.new(compare.base, project),
+ to: Commit.new(compare.head, project))
+ else
+ namespace_project_commit_url(project_namespace,
+ project, commits.first)
+ end
+ else
+ unless @action == :delete
+ namespace_project_tree_url(project_namespace,
+ project, ref_name)
+ end
+ end
+ end
+
+ def reply_to
+ if send_from_committer_email? && @notify.can_send_from_user_email?(author)
+ author.email
+ else
+ Gitlab.config.gitlab.email_reply_to
+ end
+ end
+
+ def subject
+ subject_text = '[Git]'
+ subject_text << "[#{project.path_with_namespace}]"
+ subject_text << "[#{ref_name}]" if @action == :push
+ subject_text << ' '
+
+ if @action == :push && commits
+ if commits.length > 1
+ subject_text << "Deleted " if reverse_compare?
+ subject_text << "#{commits.length} commits: #{commits.first.title}"
+ else
+ subject_text << "Deleted 1 commit: " if reverse_compare?
+ subject_text << commits.first.title
+ end
+ else
+ subject_action = action_name.dup
+ subject_action[0] = subject_action[0].capitalize
+ subject_text << "#{subject_action} #{ref_type} #{ref_name}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 496256700b8..403ebeec474 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -199,7 +199,7 @@ module Gitlab
s = s.gsub(/^#/, "\\#")
s = s.gsub(/^-/, "\\-")
s = s.gsub("`", "\\~")
- s = s.gsub("\r", "")
+ s = s.delete("\r")
s = s.gsub("\n", " \n")
s
end
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
index 8b1b6f48ed5..e0163499e30 100644
--- a/lib/gitlab/fogbugz_import/project_creator.rb
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -12,7 +12,8 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(current_user,
+ project = ::Projects::CreateService.new(
+ current_user,
name: repo.safe_name,
path: repo.path,
namespace: namespace,
diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb
index fdb6a35c78d..93c6a5bb7f5 100644
--- a/lib/gitlab/force_push_check.rb
+++ b/lib/gitlab/force_push_check.rb
@@ -7,7 +7,7 @@ module Gitlab
if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
false
else
- missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev}))
+ missed_refs, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev}))
missed_refs.split("\n").size > 0
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 0c350d7c675..f065cc5e9e9 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -20,6 +20,10 @@ module Gitlab
def blank_ref?(ref)
ref == BLANK_SHA
end
+
+ def version
+ Gitlab::VersionInfo.parse(Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --version)).first)
+ end
end
end
end
diff --git a/lib/gitlab/git/hook.rb b/lib/gitlab/git/hook.rb
index dd393fe09d2..07b856ca64c 100644
--- a/lib/gitlab/git/hook.rb
+++ b/lib/gitlab/git/hook.rb
@@ -16,6 +16,17 @@ module Gitlab
def trigger(gl_id, oldrev, newrev, ref)
return true unless exists?
+ case name
+ when "pre-receive", "post-receive"
+ call_receive_hook(gl_id, oldrev, newrev, ref)
+ when "update"
+ call_update_hook(gl_id, oldrev, newrev, ref)
+ end
+ end
+
+ private
+
+ def call_receive_hook(gl_id, oldrev, newrev, ref)
changes = [oldrev, newrev, ref].join(" ")
# function will return true if succesful
@@ -54,6 +65,12 @@ module Gitlab
exit_status
end
+
+ def call_update_hook(gl_id, oldrev, newrev, ref)
+ Dir.chdir(repo_path) do
+ system({ 'GL_ID' => gl_id }, path, ref, oldrev, newrev)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index c90184d31cf..3ed1eec517c 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -13,7 +13,7 @@ module Gitlab
def user
return @user if defined?(@user)
- @user =
+ @user =
case actor
when User
actor
@@ -125,7 +125,7 @@ module Gitlab
def change_access_check(change)
oldrev, newrev, ref = change.split(' ')
- action =
+ action =
if project.protected_branch?(branch_name(ref))
protected_branch_action(oldrev, newrev, branch_name(ref))
elsif protected_tag?(tag_name(ref))
@@ -148,7 +148,7 @@ module Gitlab
build_status_object(false, "You are not allowed to change existing tags on this project.")
else # :push_code
build_status_object(false, "You are not allowed to push code to this project.")
- end
+ end
return status
end
diff --git a/lib/gitlab/git_ref_validator.rb b/lib/gitlab/git_ref_validator.rb
index 39d17def930..4d83d8e72a8 100644
--- a/lib/gitlab/git_ref_validator.rb
+++ b/lib/gitlab/git_ref_validator.rb
@@ -6,7 +6,7 @@ module Gitlab
# Returns true for a valid reference name, false otherwise
def validate(ref_name)
Gitlab::Utils.system_silent(
- %W(git check-ref-format refs/#{ref_name}))
+ %W(#{Gitlab.config.git.bin_path} check-ref-format refs/#{ref_name}))
end
end
end
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
new file mode 100644
index 00000000000..202263c6742
--- /dev/null
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module GithubImport
+ class BaseFormatter
+ attr_reader :formatter, :project, :raw_data
+
+ def initialize(project, raw_data)
+ @project = project
+ @raw_data = raw_data
+ @formatter = Gitlab::ImportFormatter.new
+ end
+
+ private
+
+ def gl_user_id(github_id)
+ User.joins(:identities).
+ find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
+ try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 270cbcd9ccd..74d1529e1ff 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -46,7 +46,7 @@ module Gitlab
end
def github_options
- OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
+ OmniAuth::Strategies::GitHub.default_options[:client_options].to_h.symbolize_keys
end
end
end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
new file mode 100644
index 00000000000..7d58e53991a
--- /dev/null
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module GithubImport
+ class CommentFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ note: note,
+ commit_id: raw_data.commit_id,
+ line_code: line_code,
+ author_id: author_id,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
+ }
+ end
+
+ private
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def line_code
+ if on_diff?
+ Gitlab::Diff::LineCode.generate(raw_data.path, raw_data.position, 0)
+ end
+ end
+
+ def on_diff?
+ raw_data.path && raw_data.position
+ end
+
+ def note
+ formatter.author_line(author) + body
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index bd7340a80f1..2b0afbc7b39 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -12,39 +12,59 @@ module Gitlab
end
def execute
- #Issues && Comments
+ import_issues
+ import_pull_requests
+
+ true
+ end
+
+ private
+
+ def import_issues
client.list_issues(project.import_source, state: :all,
sort: :created,
- direction: :asc).each do |issue|
- if issue.pull_request.nil?
-
- body = @formatter.author_line(issue.user.login)
- body += issue.body
+ direction: :asc).each do |raw_data|
+ gh_issue = IssueFormatter.new(project, raw_data)
- if issue.comments > 0
- body += @formatter.comments_header
+ if gh_issue.valid?
+ issue = Issue.create!(gh_issue.attributes)
- client.issue_comments(project.import_source, issue.number).each do |c|
- body += @formatter.comment(c.user.login, c.created_at, c.body)
- end
+ if gh_issue.has_comments?
+ import_comments(gh_issue.number, issue)
end
+ end
+ end
+ 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)
- project.issues.create!(
- description: body,
- title: issue.title,
- state: issue.state == 'closed' ? 'closed' : 'opened',
- author_id: gl_user_id(project, issue.user.id)
- )
+ if !pull_request.cross_project? && pull_request.valid?
+ merge_request = MergeRequest.create!(pull_request.attributes)
+ import_comments(pull_request.number, merge_request)
+ import_comments_on_diff(pull_request.number, merge_request)
end
end
end
- private
+ def import_comments(issue_number, noteable)
+ comments = client.issue_comments(project.import_source, issue_number)
+ create_comments(comments, noteable)
+ end
- def gl_user_id(project, github_id)
- user = User.joins(:identities).
- find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s)
- (user && user.id) || project.creator_id
+ def import_comments_on_diff(pull_request_number, merge_request)
+ comments = client.pull_request_comments(project.import_source, pull_request_number)
+ create_comments(comments, merge_request)
+ end
+
+ def create_comments(comments, noteable)
+ comments.each do |raw_data|
+ comment = CommentFormatter.new(project, raw_data)
+ noteable.notes.create!(comment.attributes)
+ end
end
end
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
new file mode 100644
index 00000000000..1e3ba44f27c
--- /dev/null
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ module GithubImport
+ class IssueFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ title: raw_data.title,
+ description: description,
+ state: state,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ created_at: raw_data.created_at,
+ updated_at: updated_at
+ }
+ end
+
+ def has_comments?
+ raw_data.comments > 0
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def valid?
+ raw_data.pull_request.nil?
+ end
+
+ private
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gl_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ @formatter.author_line(author) + body
+ end
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'opened'
+ end
+
+ def updated_at
+ state == 'closed' ? raw_data.closed_at : raw_data.updated_at
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
new file mode 100644
index 00000000000..b7c47958cc7
--- /dev/null
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -0,0 +1,101 @@
+module Gitlab
+ module GithubImport
+ class PullRequestFormatter < BaseFormatter
+ def attributes
+ {
+ title: raw_data.title,
+ description: description,
+ source_project: source_project,
+ source_branch: source_branch.name,
+ target_project: target_project,
+ target_branch: target_branch.name,
+ state: state,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ created_at: raw_data.created_at,
+ updated_at: updated_at
+ }
+ end
+
+ def cross_project?
+ source_repo.fork == true
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def valid?
+ source_branch.present? && target_branch.present?
+ end
+
+ private
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gl_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ formatter.author_line(author) + body
+ end
+
+ def source_project
+ project
+ end
+
+ def source_repo
+ raw_data.head.repo
+ end
+
+ def source_branch
+ source_project.repository.find_branch(raw_data.head.ref)
+ end
+
+ def target_project
+ project
+ end
+
+ def target_branch
+ target_project.repository.find_branch(raw_data.base.ref)
+ end
+
+ def state
+ @state ||= case true
+ when raw_data.state == 'closed' && raw_data.merged_at.present?
+ 'merged'
+ when raw_data.state == 'closed'
+ 'closed'
+ else
+ 'opened'
+ end
+ end
+
+ def updated_at
+ case state
+ when 'merged' then raw_data.merged_at
+ when 'closed' then raw_data.closed_at
+ else
+ raw_data.updated_at
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index 9c00896c913..86fb6c51765 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -75,7 +75,7 @@ module Gitlab
end
def gitlab_options
- OmniAuth::Strategies::GitLab.default_options[:client_options].symbolize_keys
+ OmniAuth::Strategies::GitLab.default_options[:client_options].to_h.symbolize_keys
end
end
end
diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index d9452de6a50..7baaadb813c 100644
--- a/lib/gitlab/gitlab_import/project_creator.rb
+++ b/lib/gitlab/gitlab_import/project_creator.rb
@@ -11,7 +11,8 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(current_user,
+ project = ::Projects::CreateService.new(
+ current_user,
name: repo["name"],
path: repo["path"],
description: repo["description"],
diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb
index cc9a91c91f4..8e22aa9286d 100644
--- a/lib/gitlab/gitorious_import/project_creator.rb
+++ b/lib/gitlab/gitorious_import/project_creator.rb
@@ -10,7 +10,8 @@ module Gitlab
end
def execute
- ::Projects::CreateService.new(current_user,
+ ::Projects::CreateService.new(
+ current_user,
name: repo.name,
path: repo.path,
description: repo.description,
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 03c410726a5..62da327931f 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -30,7 +30,7 @@ module Gitlab
def user_map
@user_map ||= begin
- user_map = Hash.new do |hash, user|
+ user_map = Hash.new do |hash, user|
# Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked.
Client.mask_email(user).sub("...", "\\.\\.\\.")
end
@@ -76,18 +76,7 @@ module Gitlab
attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"])
body = format_issue_body(author, date, content, attachments)
-
- labels = []
- raw_issue["labels"].each do |label|
- name = nice_label_name(label)
- labels << name
-
- unless @known_labels.include?(name)
- create_label(name)
- @known_labels << name
- end
- end
- labels << nice_status_name(raw_issue["status"])
+ labels = import_issue_labels(raw_issue)
assignee_id = nil
if raw_issue.has_key?("owner")
@@ -110,6 +99,7 @@ module Gitlab
assignee_id: assignee_id,
state: raw_issue["state"] == "closed" ? "closed" : "opened"
)
+
issue.add_labels_by_names(labels)
if issue.iid != raw_issue["id"]
@@ -120,6 +110,23 @@ module Gitlab
end
end
+ def import_issue_labels(raw_issue)
+ labels = []
+
+ raw_issue["labels"].each do |label|
+ name = nice_label_name(label)
+ labels << name
+
+ unless @known_labels.include?(name)
+ create_label(name)
+ @known_labels << name
+ end
+ end
+
+ labels << nice_status_name(raw_issue["status"])
+ labels
+ end
+
def import_issue_comments(issue, comments)
Note.transaction do
while raw_comment = comments.shift
@@ -164,15 +171,13 @@ module Gitlab
when /\AMilestone:/
"#fee3ff"
- when *@closed_statuses.map { |s| nice_status_name(s) }
- "#cfcfcf"
when "Status: New"
"#428bca"
when "Status: Accepted"
"#5cb85c"
when "Status: Started"
"#8e44ad"
-
+
when "Priority: Critical"
"#ffcfcf"
when "Priority: High"
@@ -181,7 +186,7 @@ module Gitlab
"#fff5cc"
when "Priority: Low"
"#cfe9ff"
-
+
when "Type: Defect"
"#d9534f"
when "Type: Enhancement"
@@ -192,6 +197,8 @@ module Gitlab
"#8e44ad"
when "Type: Other"
"#7f8c8d"
+ when *@closed_statuses.map { |s| nice_status_name(s) }
+ "#cfcfcf"
else
"#e2e2e2"
end
@@ -220,7 +227,7 @@ module Gitlab
s = s.gsub("`", "\\`")
# Carriage returns make me sad
- s = s.gsub("\r", "")
+ s = s.delete("\r")
# Markdown ignores single newlines, but we need them as <br />.
s = s.gsub("\n", " \n")
@@ -249,8 +256,8 @@ module Gitlab
end
if raw_updates.has_key?("cc")
- cc = raw_updates["cc"].map do |l|
- deleted = l.start_with?("-")
+ cc = raw_updates["cc"].map do |l|
+ deleted = l.start_with?("-")
l = l[1..-1] if deleted
l = user_map[l]
l = "~~#{l}~~" if deleted
@@ -261,8 +268,8 @@ module Gitlab
end
if raw_updates.has_key?("labels")
- labels = raw_updates["labels"].map do |l|
- deleted = l.start_with?("-")
+ labels = raw_updates["labels"].map do |l|
+ deleted = l.start_with?("-")
l = l[1..-1] if deleted
l = nice_label_name(l)
l = "~~#{l}~~" if deleted
@@ -278,45 +285,39 @@ module Gitlab
if raw_updates.has_key?("blockedOn")
blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
- name, id = raw_blocked_on.split(":", 2)
-
- deleted = name.start_with?("-")
- name = name[1..-1] if deleted
-
- text =
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.path}/#{name}##{id}"
- end
- text = "~~#{text}~~" if deleted
- text
+ format_blocking_updates(raw_blocked_on)
end
+
updates << "*Blocked on: #{blocked_ons.join(", ")}*"
end
if raw_updates.has_key?("blocking")
blockings = raw_updates["blocking"].map do |raw_blocked_on|
- name, id = raw_blocked_on.split(":", 2)
-
- deleted = name.start_with?("-")
- name = name[1..-1] if deleted
-
- text =
- if name == project.import_source
- "##{id}"
- else
- "#{project.namespace.path}/#{name}##{id}"
- end
- text = "~~#{text}~~" if deleted
- text
+ format_blocking_updates(raw_blocked_on)
end
+
updates << "*Blocking: #{blockings.join(", ")}*"
end
updates
end
+ def format_blocking_updates(raw_blocked_on)
+ name, id = raw_blocked_on.split(":", 2)
+
+ deleted = name.start_with?("-")
+ name = name[1..-1] if deleted
+
+ text =
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ text = "~~#{text}~~" if deleted
+ text
+ end
+
def format_attachments(issue_id, comment_id, raw_attachments)
return [] unless raw_attachments
@@ -325,7 +326,7 @@ module Gitlab
filename = attachment["fileName"]
link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}"
-
+
text = "[#{filename}](#{link})"
text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/i
text
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
index 1cb7d16aeb3..87821c23460 100644
--- a/lib/gitlab/google_code_import/project_creator.rb
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -11,7 +11,8 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(current_user,
+ project = ::Projects::CreateService.new(
+ current_user,
name: repo.name,
path: repo.name,
description: repo.summary,
diff --git a/lib/gitlab/inline_diff.rb b/lib/gitlab/inline_diff.rb
index 99e7b529ba9..44507bde25d 100644
--- a/lib/gitlab/inline_diff.rb
+++ b/lib/gitlab/inline_diff.rb
@@ -11,48 +11,71 @@ module Gitlab
indexes.each do |index|
first_line = diff_arr[index+1]
second_line = diff_arr[index+2]
- max_length = [first_line.size, second_line.size].max
# Skip inline diff if empty line was replaced with content
next if first_line == "-\n"
- first_the_same_symbols = 0
- (0..max_length + 1).each do |i|
- first_the_same_symbols = i - 1
- if first_line[i] != second_line[i] && i > 0
- break
- end
- end
+ first_token = find_first_token(first_line, second_line)
+ apply_first_token(diff_arr, index, first_token)
+
+ last_token = find_last_token(first_line, second_line, first_token)
+ apply_last_token(diff_arr, index, last_token)
+ end
+
+ diff_arr
+ end
+
+ def apply_first_token(diff_arr, index, first_token)
+ start = first_token + START
+
+ if first_token.empty?
+ # In case if we remove string of spaces in commit
+ diff_arr[index+1].sub!("-", "-" => "-#{START}")
+ diff_arr[index+2].sub!("+", "+" => "+#{START}")
+ else
+ diff_arr[index+1].sub!(first_token, first_token => start)
+ diff_arr[index+2].sub!(first_token, first_token => start)
+ end
+ end
- first_token = first_line[0..first_the_same_symbols][1..-1]
- start = first_token + START
+ def apply_last_token(diff_arr, index, last_token)
+ # This is tricky: escape backslashes so that `sub` doesn't interpret them
+ # as backreferences. Regexp.escape does NOT do the right thing.
+ replace_token = FINISH + last_token.gsub(/\\/, '\&\&')
+ diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
+ diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
+ end
+
+ def find_first_token(first_line, second_line)
+ max_length = [first_line.size, second_line.size].max
+ first_the_same_symbols = 0
+
+ (0..max_length + 1).each do |i|
+ first_the_same_symbols = i - 1
- if first_token.empty?
- # In case if we remove string of spaces in commit
- diff_arr[index+1].sub!("-", "-" => "-#{START}")
- diff_arr[index+2].sub!("+", "+" => "+#{START}")
- else
- diff_arr[index+1].sub!(first_token, first_token => start)
- diff_arr[index+2].sub!(first_token, first_token => start)
+ if first_line[i] != second_line[i] && i > 0
+ break
end
+ end
+
+ first_line[0..first_the_same_symbols][1..-1]
+ end
+
+ def find_last_token(first_line, second_line, first_token)
+ max_length = [first_line.size, second_line.size].max
+ last_the_same_symbols = 0
+
+ (1..max_length + 1).each do |i|
+ last_the_same_symbols = -i
+ shortest_line = second_line.size > first_line.size ? first_line : second_line
- last_the_same_symbols = 0
- (1..max_length + 1).each do |i|
- last_the_same_symbols = -i
- shortest_line = second_line.size > first_line.size ? first_line : second_line
- if ( first_line[-i] != second_line[-i] ) || "#{first_token}#{START}".size == shortest_line[1..-i].size
- break
- end
+ if (first_line[-i] != second_line[-i]) || "#{first_token}#{START}".size == shortest_line[1..-i].size
+ break
end
- last_the_same_symbols += 1
- last_token = first_line[last_the_same_symbols..-1]
- # This is tricky: escape backslashes so that `sub` doesn't interpret them
- # as backreferences. Regexp.escape does NOT do the right thing.
- replace_token = FINISH + last_token.gsub(/\\/, '\&\&')
- diff_arr[index+1].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
- diff_arr[index+2].sub!(/#{Regexp.escape(last_token)}$/, replace_token)
end
- diff_arr
+
+ last_the_same_symbols += 1
+ first_line[last_the_same_symbols..-1]
end
def _indexes_of_changed_lines(diff_arr)
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 16ff03c38d4..c438a3d167b 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -37,13 +37,15 @@ module Gitlab
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- user.block unless user.blocked?
+ user.block
false
else
user.activate if user.blocked? && !ldap_config.block_auto_created_users
true
end
else
+ # Block the user if they no longer exist in LDAP/AD
+ user.block
false
end
rescue
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 4be99dd88c2..aef08c97d1d 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -14,7 +14,7 @@ module Gitlab
# LDAP distinguished name is case-insensitive
identity = ::Identity.
where(provider: provider).
- where('lower(extern_uid) = ?', uid.mb_chars.downcase.to_s).last
+ iwhere(extern_uid: uid).last
identity && identity.user
end
end
@@ -31,7 +31,7 @@ module Gitlab
def find_by_uid_and_provider
self.class.find_by_uid_and_provider(
- auth_hash.uid.downcase, auth_hash.provider)
+ auth_hash.uid, auth_hash.provider)
end
def find_by_email
@@ -47,7 +47,7 @@ module Gitlab
# find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
identity ||= gl_user.identities.build(provider: auth_hash.provider)
-
+
# For a new user set extern_uid to the LDAP DN
# For an existing user with matching email but changed DN, update the DN.
# For an existing user with no change in DN, this line changes nothing.
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
new file mode 100644
index 00000000000..9d9617761b3
--- /dev/null
+++ b/lib/gitlab/lfs/response.rb
@@ -0,0 +1,327 @@
+module Gitlab
+ module Lfs
+ class Response
+
+ def initialize(project, user, request)
+ @origin_project = project
+ @project = storage_project(project)
+ @user = user
+ @env = request.env
+ @request = request
+ end
+
+ def render_download_object_response(oid)
+ render_response_to_download do
+ if check_download_sendfile_header?
+ render_lfs_sendfile(oid)
+ else
+ render_not_found
+ end
+ end
+ end
+
+ def render_batch_operation_response
+ request_body = JSON.parse(@request.body.read)
+ case request_body["operation"]
+ when "download"
+ render_batch_download(request_body)
+ when "upload"
+ render_batch_upload(request_body)
+ else
+ render_not_found
+ end
+ end
+
+ def render_storage_upload_authorize_response(oid, size)
+ render_response_to_push do
+ [
+ 200,
+ { "Content-Type" => "application/json; charset=utf-8" },
+ [JSON.dump({
+ 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ 'LfsOid' => oid,
+ 'LfsSize' => size
+ })]
+ ]
+ end
+ end
+
+ def render_storage_upload_store_response(oid, size, tmp_file_name)
+ render_response_to_push do
+ render_lfs_upload_ok(oid, size, tmp_file_name)
+ end
+ end
+
+ def render_unsupported_deprecated_api
+ [
+ 501,
+ { "Content-Type" => "application/json; charset=utf-8" },
+ [JSON.dump({
+ '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",
+ })]
+ ]
+ end
+
+ private
+
+ def render_not_enabled
+ [
+ 501,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ },
+ [JSON.dump({
+ 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_unauthorized
+ [
+ 401,
+ {
+ 'Content-Type' => 'text/plain'
+ },
+ ['Unauthorized']
+ ]
+ end
+
+ def render_not_found
+ [
+ 404,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json"
+ },
+ [JSON.dump({
+ 'message' => 'Not found.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_forbidden
+ [
+ 403,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json"
+ },
+ [JSON.dump({
+ 'message' => 'Access forbidden. Check your access level.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_lfs_sendfile(oid)
+ return render_not_found unless oid.present?
+
+ lfs_object = object_for_download(oid)
+
+ if lfs_object && lfs_object.file.exists?
+ [
+ 200,
+ {
+ # GitLab-workhorse will forward Content-Type header
+ "Content-Type" => "application/octet-stream",
+ "X-Sendfile" => lfs_object.file.path
+ },
+ []
+ ]
+ else
+ render_not_found
+ end
+ end
+
+ def render_batch_upload(body)
+ return render_not_found if body.empty? || body['objects'].nil?
+
+ render_response_to_push do
+ response = build_upload_batch_response(body['objects'])
+ [
+ 200,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ "Cache-Control" => "private",
+ },
+ [JSON.dump(response)]
+ ]
+ end
+ end
+
+ def render_batch_download(body)
+ return render_not_found if body.empty? || body['objects'].nil?
+
+ render_response_to_download do
+ response = build_download_batch_response(body['objects'])
+ [
+ 200,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ "Cache-Control" => "private",
+ },
+ [JSON.dump(response)]
+ ]
+ end
+ end
+
+ def render_lfs_upload_ok(oid, size, tmp_file)
+ if store_file(oid, size, tmp_file)
+ [
+ 200,
+ {
+ 'Content-Type' => 'text/plain',
+ 'Content-Length' => 0
+ },
+ []
+ ]
+ else
+ [
+ 422,
+ { 'Content-Type' => 'text/plain' },
+ ["Unprocessable entity"]
+ ]
+ end
+ end
+
+ def render_response_to_download
+ return render_not_enabled unless Gitlab.config.lfs.enabled
+
+ unless @project.public?
+ return render_unauthorized unless @user
+ return render_forbidden unless user_can_fetch?
+ end
+
+ yield
+ end
+
+ def render_response_to_push
+ return render_not_enabled unless Gitlab.config.lfs.enabled
+ return render_unauthorized unless @user
+ return render_forbidden unless user_can_push?
+
+ yield
+ end
+
+ def check_download_sendfile_header?
+ @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
+ end
+
+ def user_can_fetch?
+ # Check user access against the project they used to initiate the pull
+ @user.can?(:download_code, @origin_project)
+ end
+
+ def user_can_push?
+ # Check user access against the project they used to initiate the push
+ @user.can?(:push_code, @origin_project)
+ end
+
+ def storage_project(project)
+ if project.forked?
+ storage_project(project.forked_from_project)
+ else
+ project
+ end
+ end
+
+ def store_file(oid, size, tmp_file)
+ tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
+
+ object = LfsObject.find_or_create_by(oid: oid, size: size)
+ if object.file.exists?
+ success = true
+ else
+ success = move_tmp_file_to_storage(object, tmp_file_path)
+ end
+
+ if success
+ success = link_to_project(object)
+ end
+
+ success
+ ensure
+ # Ensure that the tmp file is removed
+ FileUtils.rm_f(tmp_file_path)
+ end
+
+ def object_for_download(oid)
+ @project.lfs_objects.find_by(oid: oid)
+ end
+
+ def move_tmp_file_to_storage(object, path)
+ File.open(path) do |f|
+ object.file = f
+ end
+
+ object.file.store!
+ object.save
+ end
+
+ def link_to_project(object)
+ if object && !object.projects.exists?(@project.id)
+ object.projects << @project
+ object.save
+ end
+ end
+
+ def select_existing_objects(objects)
+ objects_oids = objects.map { |o| o['oid'] }
+ @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
+ end
+
+ def build_upload_batch_response(objects)
+ selected_objects = select_existing_objects(objects)
+
+ upload_hypermedia_links(objects, selected_objects)
+ end
+
+ def build_download_batch_response(objects)
+ selected_objects = select_existing_objects(objects)
+
+ download_hypermedia_links(objects, selected_objects)
+ end
+
+ def download_hypermedia_links(all_objects, existing_objects)
+ all_objects.each do |object|
+ if existing_objects.include?(object['oid'])
+ object['actions'] = {
+ 'download' => {
+ 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}",
+ 'header' => {
+ 'Authorization' => @env['HTTP_AUTHORIZATION']
+ }.compact
+ }
+ }
+ else
+ object['error'] = {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ end
+ end
+
+ { 'objects' => all_objects }
+ end
+
+ def upload_hypermedia_links(all_objects, existing_objects)
+ all_objects.each do |object|
+ # generate actions only for non-existing objects
+ next if existing_objects.include?(object['oid'])
+
+ object['actions'] = {
+ 'upload' => {
+ 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
+ 'header' => {
+ 'Authorization' => @env['HTTP_AUTHORIZATION']
+ }.compact
+ }
+ }
+ end
+
+ { 'objects' => all_objects }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
new file mode 100644
index 00000000000..78d02891102
--- /dev/null
+++ b/lib/gitlab/lfs/router.rb
@@ -0,0 +1,97 @@
+module Gitlab
+ module Lfs
+ class Router
+ def initialize(project, user, request)
+ @project = project
+ @user = user
+ @env = request.env
+ @request = request
+ end
+
+ def try_call
+ return unless @request && @request.path.present?
+
+ case @request.request_method
+ when 'GET'
+ get_response
+ when 'POST'
+ post_response
+ when 'PUT'
+ put_response
+ else
+ nil
+ end
+ end
+
+ private
+
+ def get_response
+ path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/)
+ return nil unless path_match
+
+ oid = path_match[2]
+ return nil unless oid
+
+ case path_match[1]
+ when "info/lfs"
+ lfs.render_unsupported_deprecated_api
+ when "gitlab-lfs"
+ lfs.render_download_object_response(oid)
+ else
+ nil
+ end
+ end
+
+ def post_response
+ post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/)
+ return nil unless post_path
+
+ # Check for Batch API
+ if post_path[0].ends_with?("/info/lfs/objects/batch")
+ lfs.render_batch_operation_response
+ elsif post_path[0].ends_with?("/info/lfs/objects")
+ lfs.render_unsupported_deprecated_api
+ else
+ nil
+ end
+ end
+
+ def put_response
+ object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/)
+ return nil if object_match.nil?
+
+ oid = object_match[1]
+ size = object_match[2].try(:to_i)
+ return nil if oid.nil? || size.nil?
+
+ # GitLab-workhorse requests
+ # 1. Try to authorize the request
+ # 2. send a request with a header containing the name of the temporary file
+ if object_match[3] && object_match[3] == '/authorize'
+ lfs.render_storage_upload_authorize_response(oid, size)
+ else
+ tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP'])
+ return nil unless tmp_file_name
+
+ lfs.render_storage_upload_store_response(oid, size, tmp_file_name)
+ end
+ end
+
+ def lfs
+ return unless @project
+
+ Gitlab::Lfs::Response.new(@project, @user, @request)
+ end
+
+ def sanitize_tmp_filename(name)
+ if name.present?
+ name.gsub!(/^.*(\\|\/)/, '')
+ name = name.match(/[0-9a-f]{73}/)
+ name[0] if name
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
deleted file mode 100644
index b082bfc434b..00000000000
--- a/lib/gitlab/markdown.rb
+++ /dev/null
@@ -1,200 +0,0 @@
-require 'html/pipeline'
-
-module Gitlab
- # Custom parser for GitLab-flavored Markdown
- #
- # See the files in `lib/gitlab/markdown/` for specific processing information.
- module Markdown
- # Convert a Markdown String into an HTML-safe String of HTML
- #
- # Note that while the returned HTML will have been sanitized of dangerous
- # HTML, it may post a risk of information leakage if it's not also passed
- # through `post_process`.
- #
- # Also note that the returned String is always HTML, not XHTML. Views
- # requiring XHTML, such as Atom feeds, need to call `post_process` on the
- # result, providing the appropriate `pipeline` option.
- #
- # markdown - Markdown String
- # context - Hash of context options passed to our HTML Pipeline
- #
- # Returns an HTML-safe String
- def self.render(markdown, context = {})
- html = renderer.render(markdown)
- html = gfm(html, context)
-
- html.html_safe
- end
-
- # Convert a Markdown String into HTML without going through the HTML
- # Pipeline.
- #
- # Note that because the pipeline is skipped, SanitizationFilter is as well.
- # Do not output the result of this method to the user.
- #
- # markdown - Markdown String
- #
- # Returns a String
- def self.render_without_gfm(markdown)
- renderer.render(markdown)
- end
-
- # Perform post-processing on an HTML String
- #
- # This method is used to perform state-dependent changes to a String of
- # HTML, such as removing references that the current user doesn't have
- # permission to make (`RedactorFilter`).
- #
- # html - String to process
- # options - Hash of options to customize output
- # :pipeline - Symbol pipeline type
- # :project - Project
- # :user - User object
- #
- # Returns an HTML-safe String
- def self.post_process(html, options)
- context = {
- project: options[:project],
- current_user: options[:user]
- }
- doc = post_processor.to_document(html, context)
-
- if options[:pipeline] == :atom
- doc.to_html(save_with: Nokogiri::XML::Node::SaveOptions::AS_XHTML)
- else
- doc.to_html
- end.html_safe
- end
-
- # Provide autoload paths for filters to prevent a circular dependency error
- autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter'
- autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter'
- autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter'
- autoload :EmojiFilter, 'gitlab/markdown/emoji_filter'
- autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter'
- autoload :ExternalLinkFilter, 'gitlab/markdown/external_link_filter'
- autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
- autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
- autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
- autoload :RedactorFilter, 'gitlab/markdown/redactor_filter'
- autoload :RelativeLinkFilter, 'gitlab/markdown/relative_link_filter'
- autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
- autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
- autoload :SyntaxHighlightFilter, 'gitlab/markdown/syntax_highlight_filter'
- autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter'
- autoload :TaskListFilter, 'gitlab/markdown/task_list_filter'
- autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter'
- autoload :UploadLinkFilter, 'gitlab/markdown/upload_link_filter'
-
- # Public: Parse the provided HTML with GitLab-Flavored Markdown
- #
- # html - HTML String
- # options - A Hash of options used to customize output (default: {})
- # :no_header_anchors - Disable header anchors in TableOfContentsFilter
- # :path - Current path String
- # :pipeline - Symbol pipeline type
- # :project - Current Project object
- # :project_wiki - Current ProjectWiki object
- # :ref - Current ref String
- #
- # Returns an HTML-safe String
- def self.gfm(html, options = {})
- return '' unless html.present?
-
- @pipeline ||= HTML::Pipeline.new(filters)
-
- context = {
- # SanitizationFilter
- pipeline: options[:pipeline],
-
- # EmojiFilter
- asset_host: Gitlab::Application.config.asset_host,
- asset_root: Gitlab.config.gitlab.base_url,
-
- # ReferenceFilter
- only_path: only_path_pipeline?(options[:pipeline]),
- project: options[:project],
-
- # RelativeLinkFilter
- project_wiki: options[:project_wiki],
- ref: options[:ref],
- requested_path: options[:path],
-
- # TableOfContentsFilter
- no_header_anchors: options[:no_header_anchors]
- }
-
- @pipeline.to_html(html, context).html_safe
- end
-
- private
-
- # Check if a pipeline enables the `only_path` context option
- #
- # Returns Boolean
- def self.only_path_pipeline?(pipeline)
- case pipeline
- when :atom, :email
- false
- else
- true
- end
- end
-
- def self.redcarpet_options
- # https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
- @redcarpet_options ||= {
- fenced_code_blocks: true,
- footnotes: true,
- lax_spacing: true,
- no_intra_emphasis: true,
- space_after_headers: true,
- strikethrough: true,
- superscript: true,
- tables: true
- }.freeze
- end
-
- def self.renderer
- @markdown ||= begin
- renderer = Redcarpet::Render::HTML.new
- Redcarpet::Markdown.new(renderer, redcarpet_options)
- end
- end
-
- def self.post_processor
- @post_processor ||= HTML::Pipeline.new([Gitlab::Markdown::RedactorFilter])
- end
-
- # Filters used in our pipeline
- #
- # SanitizationFilter should come first so that all generated reference HTML
- # goes through untouched.
- #
- # See https://github.com/jch/html-pipeline#filters for more filters.
- def self.filters
- [
- Gitlab::Markdown::SyntaxHighlightFilter,
- Gitlab::Markdown::SanitizationFilter,
-
- Gitlab::Markdown::UploadLinkFilter,
- Gitlab::Markdown::RelativeLinkFilter,
- Gitlab::Markdown::EmojiFilter,
- Gitlab::Markdown::TableOfContentsFilter,
- Gitlab::Markdown::AutolinkFilter,
- Gitlab::Markdown::ExternalLinkFilter,
-
- Gitlab::Markdown::UserReferenceFilter,
- Gitlab::Markdown::IssueReferenceFilter,
- Gitlab::Markdown::ExternalIssueReferenceFilter,
- Gitlab::Markdown::MergeRequestReferenceFilter,
- Gitlab::Markdown::SnippetReferenceFilter,
- Gitlab::Markdown::CommitRangeReferenceFilter,
- Gitlab::Markdown::CommitReferenceFilter,
- Gitlab::Markdown::LabelReferenceFilter,
-
- Gitlab::Markdown::TaskListFilter
- ]
- end
- end
-end
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
deleted file mode 100644
index e070edae0a4..00000000000
--- a/lib/gitlab/markdown/commit_range_reference_filter.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'gitlab/markdown'
-
-module Gitlab
- module Markdown
- # HTML filter that replaces commit range references with links.
- #
- # This filter supports cross-project references.
- class CommitRangeReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find commit range references in text
- #
- # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
- # "<a href=...>#{commit_range}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the String commit range, and an optional String
- # of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(CommitRange.reference_pattern) do |match|
- yield match, $~[:commit_range], $~[:project]
- 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 = CommitRange.new(id, project)
-
- return unless range.valid_commits?
-
- { commit_range: range }
- end
-
- def initialize(*args)
- super
-
- @commit_map = {}
- end
-
- def call
- replace_text_nodes_matching(CommitRange.reference_pattern) do |content|
- commit_range_link_filter(content)
- end
- end
-
- # Replace commit range references in text with links to compare the commit
- # ranges.
- #
- # text - String text to replace references in.
- #
- # Returns a String with commit range references replaced with links. All
- # links have `gfm` and `gfm-commit_range` class names attached for
- # styling.
- def commit_range_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- range = CommitRange.new(id, project)
-
- if range.valid_commits?
- url = url_for_commit_range(project, range)
-
- title = range.reference_title
- klass = reference_class(:commit_range)
- data = data_attribute(project: project.id, commit_range: id)
-
- project_ref += '@' if project_ref
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{project_ref}#{range}</a>)
- else
- match
- end
- end
- end
-
- def url_for_commit_range(project, range)
- h = Gitlab::Application.routes.url_helpers
- h.namespace_project_compare_url(project.namespace, project,
- range.to_param.merge(only_path: context[:only_path]))
- end
- end
- end
-end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
deleted file mode 100644
index 8cdbeb1f9cf..00000000000
--- a/lib/gitlab/markdown/commit_reference_filter.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-require 'gitlab/markdown'
-
-module Gitlab
- module Markdown
- # HTML filter that replaces commit references with links.
- #
- # This filter supports cross-project references.
- class CommitReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find commit references in text
- #
- # CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
- # "<a href=...>#{commit}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the String commit identifier, and an optional
- # String of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Commit.reference_pattern) do |match|
- yield match, $~[:commit], $~[:project]
- 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 = commit_from_ref(project, id)
-
- return unless commit
-
- { commit: commit }
- end
-
- def call
- replace_text_nodes_matching(Commit.reference_pattern) do |content|
- commit_link_filter(content)
- end
- end
-
- # Replace commit references in text with links to the commit specified.
- #
- # text - String text to replace references in.
- #
- # Returns a String with commit references replaced with links. All links
- # have `gfm` and `gfm-commit` class names attached for styling.
- def commit_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if commit = self.class.commit_from_ref(project, id)
- url = url_for_commit(project, commit)
-
- title = escape_once(commit.link_title)
- klass = reference_class(:commit)
- data = data_attribute(project: project.id, commit: id)
-
- project_ref += '@' if project_ref
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{project_ref}#{commit.short_id}</a>)
- else
- match
- end
- end
- end
-
- def self.commit_from_ref(project, id)
- if project && project.valid_repo?
- project.commit(id)
- end
- end
-
- def url_for_commit(project, commit)
- h = Gitlab::Application.routes.url_helpers
- h.namespace_project_commit_url(project.namespace, project, commit,
- only_path: context[:only_path])
- end
- end
- end
-end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
deleted file mode 100644
index 6ab04a584b0..00000000000
--- a/lib/gitlab/markdown/cross_project_reference.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'gitlab/markdown'
-
-module Gitlab
- module Markdown
- # Common methods for ReferenceFilters that support an optional cross-project
- # reference.
- module CrossProjectReference
- # Given a cross-project reference string, get the Project record
- #
- # Defaults to value of `context[:project]` if:
- # * No reference is given OR
- # * Reference given doesn't exist
- #
- # ref - String reference.
- #
- # Returns a Project, or nil if the reference can't be found
- def project_from_ref(ref)
- return context[:project] unless ref
-
- Project.find_with_namespace(ref)
- end
- end
- end
-end
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
deleted file mode 100644
index 481d282f7b1..00000000000
--- a/lib/gitlab/markdown/issue_reference_filter.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-require 'gitlab/markdown'
-
-module Gitlab
- module Markdown
- # HTML filter that replaces issue references with links. References to
- # issues that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class IssueReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find `#123` issue references in text
- #
- # IssueReferenceFilter.references_in(text) do |match, issue, project_ref|
- # "<a href=...>##{issue}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer issue ID, and an optional String of
- # the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Issue.reference_pattern) do |match|
- yield match, $~[:issue].to_i, $~[:project]
- end
- end
-
- def self.referenced_by(node)
- { issue: LazyReference.new(Issue, node.attr("data-issue")) }
- end
-
- def call
- replace_text_nodes_matching(Issue.reference_pattern) do |content|
- issue_link_filter(content)
- end
- end
-
- # Replace `#123` issue references in text with links to the referenced
- # issue's details page.
- #
- # text - String text to replace references in.
- #
- # Returns a String with `#123` references replaced with links. All links
- # have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if project && issue = project.get_issue(id)
- url = url_for_issue(id, project, only_path: context[:only_path])
-
- title = escape_once("Issue: #{issue.title}")
- klass = reference_class(:issue)
- data = data_attribute(project: project.id, issue: issue.id)
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
- else
- match
- end
- end
- end
-
- def url_for_issue(*args)
- IssuesHelper.url_for_issue(*args)
- end
- end
- end
-end
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
deleted file mode 100644
index 5bc63269808..00000000000
--- a/lib/gitlab/markdown/merge_request_reference_filter.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'gitlab/markdown'
-
-module Gitlab
- module Markdown
- # HTML filter that replaces merge request references with links. References
- # to merge requests that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class MergeRequestReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find `!123` merge request references in text
- #
- # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref|
- # "<a href=...>##{merge_request}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer merge request ID, and an optional
- # String of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(MergeRequest.reference_pattern) do |match|
- yield match, $~[:merge_request].to_i, $~[:project]
- end
- end
-
- def self.referenced_by(node)
- { merge_request: LazyReference.new(MergeRequest, node.attr("data-merge-request")) }
- end
-
- def call
- replace_text_nodes_matching(MergeRequest.reference_pattern) do |content|
- merge_request_link_filter(content)
- end
- end
-
- # Replace `!123` merge request references in text with links to the
- # referenced merge request's details page.
- #
- # text - String text to replace references in.
- #
- # Returns a String with `!123` references replaced with links. All links
- # have `gfm` and `gfm-merge_request` class names attached for styling.
- def merge_request_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if project && merge_request = project.merge_requests.find_by(iid: id)
- title = escape_once("Merge Request: #{merge_request.title}")
- klass = reference_class(:merge_request)
- data = data_attribute(project: project.id, merge_request: merge_request.id)
-
- url = url_for_merge_request(merge_request, project)
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
- else
- match
- end
- end
- end
-
- def url_for_merge_request(mr, project)
- h = Gitlab::Application.routes.url_helpers
- h.namespace_project_merge_request_url(project.namespace, project, mr,
- only_path: context[:only_path])
- end
- end
- end
-end
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
new file mode 100644
index 00000000000..8f3f43c0e91
--- /dev/null
+++ b/lib/gitlab/markdown/pipeline.rb
@@ -0,0 +1,34 @@
+require 'banzai'
+
+module Gitlab
+ module Markdown
+ class Pipeline
+ def self.[](name)
+ name ||= :full
+ const_get("#{name.to_s.camelize}Pipeline")
+ end
+
+ def self.filters
+ []
+ end
+
+ def self.transform_context(context)
+ context
+ end
+
+ def self.html_pipeline
+ @html_pipeline ||= HTML::Pipeline.new(filters)
+ end
+
+ class << self
+ %i(call to_document to_html).each do |meth|
+ define_method(meth) do |text, context|
+ context = transform_context(context)
+
+ html_pipeline.send(meth, text, context)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
deleted file mode 100644
index f783f951711..00000000000
--- a/lib/gitlab/markdown/snippet_reference_filter.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'gitlab/markdown'
-
-module Gitlab
- module Markdown
- # HTML filter that replaces snippet references with links. References to
- # snippets that do not exist are ignored.
- #
- # This filter supports cross-project references.
- class SnippetReferenceFilter < ReferenceFilter
- include CrossProjectReference
-
- # Public: Find `$123` snippet references in text
- #
- # SnippetReferenceFilter.references_in(text) do |match, snippet|
- # "<a href=...>$#{snippet}</a>"
- # end
- #
- # text - String text to search.
- #
- # Yields the String match, the Integer snippet ID, and an optional String
- # of the external project reference.
- #
- # Returns a String replaced with the return of the block.
- def self.references_in(text)
- text.gsub(Snippet.reference_pattern) do |match|
- yield match, $~[:snippet].to_i, $~[:project]
- end
- end
-
- def self.referenced_by(node)
- { snippet: LazyReference.new(Snippet, node.attr("data-snippet")) }
- end
-
- def call
- replace_text_nodes_matching(Snippet.reference_pattern) do |content|
- snippet_link_filter(content)
- end
- end
-
- # Replace `$123` snippet references in text with links to the referenced
- # snippets's details page.
- #
- # text - String text to replace references in.
- #
- # Returns a String with `$123` references replaced with links. All links
- # have `gfm` and `gfm-snippet` class names attached for styling.
- def snippet_link_filter(text)
- self.class.references_in(text) do |match, id, project_ref|
- project = self.project_from_ref(project_ref)
-
- if project && snippet = project.snippets.find_by(id: id)
- title = escape_once("Snippet: #{snippet.title}")
- klass = reference_class(:snippet)
- data = data_attribute(project: project.id, snippet: snippet.id)
-
- url = url_for_snippet(snippet, project)
-
- %(<a href="#{url}" #{data}
- title="#{title}"
- class="#{klass}">#{match}</a>)
- else
- match
- end
- end
- end
-
- def url_for_snippet(snippet, project)
- h = Gitlab::Application.routes.url_helpers
- h.namespace_project_snippet_url(project.namespace, project, snippet,
- only_path: context[:only_path])
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
new file mode 100644
index 00000000000..ee88ab34d6c
--- /dev/null
+++ b/lib/gitlab/metrics.rb
@@ -0,0 +1,102 @@
+module Gitlab
+ module Metrics
+ extend Gitlab::CurrentSettings
+
+ RAILS_ROOT = Rails.root.to_s
+ METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
+ PATH_REGEX = /^#{RAILS_ROOT}\/?/
+
+ def self.settings
+ @settings ||= {
+ enabled: current_application_settings[:metrics_enabled],
+ pool_size: current_application_settings[:metrics_pool_size],
+ timeout: current_application_settings[:metrics_timeout],
+ method_call_threshold: current_application_settings[:metrics_method_call_threshold],
+ host: current_application_settings[:metrics_host],
+ username: current_application_settings[:metrics_username],
+ password: current_application_settings[:metrics_password],
+ port: current_application_settings[:metrics_port]
+ }
+ end
+
+ def self.enabled?
+ settings[:enabled] || false
+ end
+
+ def self.mri?
+ RUBY_ENGINE == 'ruby'
+ end
+
+ def self.method_call_threshold
+ # This is memoized since this method is called for every instrumented
+ # method. Loading data from an external cache on every method call slows
+ # things down too much.
+ @method_call_threshold ||= settings[:method_call_threshold]
+ end
+
+ def self.pool
+ @pool
+ end
+
+ # Returns a relative path and line number based on the last application call
+ # frame.
+ def self.last_relative_application_frame
+ frame = caller_locations.find do |l|
+ l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT)
+ end
+
+ if frame
+ return frame.path.sub(PATH_REGEX, ''), frame.lineno
+ else
+ return nil, nil
+ end
+ end
+
+ def self.submit_metrics(metrics)
+ prepared = prepare_metrics(metrics)
+
+ pool.with do |connection|
+ prepared.each do |metric|
+ begin
+ connection.write_points([metric])
+ rescue StandardError
+ end
+ end
+ end
+ end
+
+ def self.prepare_metrics(metrics)
+ metrics.map do |hash|
+ new_hash = hash.symbolize_keys
+
+ new_hash[:tags].each do |key, value|
+ if value.blank?
+ new_hash[:tags].delete(key)
+ else
+ new_hash[:tags][key] = escape_value(value)
+ end
+ end
+
+ new_hash
+ end
+ end
+
+ def self.escape_value(value)
+ value.to_s.gsub('=', '\\=')
+ end
+
+ # When enabled this should be set before being used as the usual pattern
+ # "@foo ||= bar" is _not_ thread-safe.
+ if enabled?
+ @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do
+ host = settings[:host]
+ user = settings[:username]
+ pw = settings[:password]
+ port = settings[:port]
+
+ InfluxDB::Client.
+ new(udp: { host: host, port: port }, username: user, password: pw)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/delta.rb b/lib/gitlab/metrics/delta.rb
new file mode 100644
index 00000000000..bcf28eed84d
--- /dev/null
+++ b/lib/gitlab/metrics/delta.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module Metrics
+ # Class for calculating the difference between two numeric values.
+ #
+ # Every call to `compared_with` updates the internal value. This makes it
+ # possible to use a single Delta instance to calculate the delta over time
+ # of an ever increasing number.
+ #
+ # Example usage:
+ #
+ # delta = Delta.new(0)
+ #
+ # delta.compared_with(10) # => 10
+ # delta.compared_with(15) # => 5
+ # delta.compared_with(20) # => 5
+ class Delta
+ def initialize(value = 0)
+ @value = value
+ end
+
+ # new_value - The value to compare with as a Numeric.
+ #
+ # Returns a new Numeric (depending on the type of `new_value`).
+ def compared_with(new_value)
+ delta = new_value - @value
+ @value = new_value
+
+ delta
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
new file mode 100644
index 00000000000..d9fce2e6758
--- /dev/null
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -0,0 +1,148 @@
+module Gitlab
+ module Metrics
+ # Module for instrumenting methods.
+ #
+ # This module allows instrumenting of methods without having to actually
+ # alter the target code (e.g. by including modules).
+ #
+ # Example usage:
+ #
+ # Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
+ module Instrumentation
+ SERIES = 'method_calls'
+
+ def self.configure
+ yield self
+ end
+
+ # Instruments a class method.
+ #
+ # mod - The module to instrument as a Module/Class.
+ # name - The name of the method to instrument.
+ def self.instrument_method(mod, name)
+ instrument(:class, mod, name)
+ end
+
+ # Instruments an instance method.
+ #
+ # mod - The module to instrument as a Module/Class.
+ # name - The name of the method to instrument.
+ def self.instrument_instance_method(mod, name)
+ instrument(:instance, mod, name)
+ end
+
+ # Recursively instruments all subclasses of the given root module.
+ #
+ # This can be used to for example instrument all ActiveRecord models (as
+ # these all inherit from ActiveRecord::Base).
+ #
+ # This method can optionally take a block to pass to `instrument_methods`
+ # and `instrument_instance_methods`.
+ #
+ # root - The root module for which to instrument subclasses. The root
+ # module itself is not instrumented.
+ def self.instrument_class_hierarchy(root, &block)
+ visit = root.subclasses
+
+ until visit.empty?
+ klass = visit.pop
+
+ instrument_methods(klass, &block)
+ instrument_instance_methods(klass, &block)
+
+ klass.subclasses.each { |c| visit << c }
+ end
+ end
+
+ # Instruments all public 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
+ # module and an UnboundMethod. If the block returns a non truthy value the
+ # method is not instrumented.
+ #
+ # mod - The module to instrument.
+ def self.instrument_methods(mod)
+ mod.public_methods(false).each do |name|
+ method = mod.method(name)
+
+ if method.owner == mod.singleton_class
+ if !block_given? || block_given? && yield(mod, method)
+ instrument_method(mod, name)
+ end
+ end
+ end
+ end
+
+ # Instruments all public 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|
+ method = mod.instance_method(name)
+
+ if method.owner == mod
+ if !block_given? || block_given? && yield(mod, method)
+ instrument_instance_method(mod, name)
+ end
+ end
+ end
+ end
+
+ # Instruments a method.
+ #
+ # type - The type (:class or :instance) of method to instrument.
+ # mod - The module containing the method.
+ # name - The name of the method to instrument.
+ def self.instrument(type, mod, name)
+ return unless Metrics.enabled?
+
+ name = name.to_sym
+ alias_name = :"_original_#{name}"
+ target = type == :instance ? mod : mod.singleton_class
+
+ if type == :instance
+ target = mod
+ label = "#{mod.name}##{name}"
+ else
+ target = mod.singleton_class
+ label = "#{mod.name}.#{name}"
+ end
+
+ target.class_eval <<-EOF, __FILE__, __LINE__ + 1
+ alias_method #{alias_name.inspect}, #{name.inspect}
+
+ def #{name}(*args, &block)
+ trans = Gitlab::Metrics::Instrumentation.transaction
+
+ if trans
+ start = Time.now
+ retval = __send__(#{alias_name.inspect}, *args, &block)
+ 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
+ else
+ __send__(#{alias_name.inspect}, *args, &block)
+ end
+ end
+ EOF
+ end
+
+ # Small layer of indirection to make it easier to stub out the current
+ # transaction.
+ def self.transaction
+ Transaction.current
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
new file mode 100644
index 00000000000..7ea9555cc8c
--- /dev/null
+++ b/lib/gitlab/metrics/metric.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module Metrics
+ # Class for storing details of a single metric (label, value, etc).
+ class Metric
+ attr_reader :series, :values, :tags, :created_at
+
+ # 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
+ end
+
+ # Returns a Hash in a format that can be directly written to InfluxDB.
+ def to_hash
+ {
+ series: @series,
+ tags: @tags,
+ values: @values,
+ timestamp: @created_at.to_i * 1_000_000_000
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
new file mode 100644
index 00000000000..5c0587c4c51
--- /dev/null
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Metrics
+ # Rack middleware for tracking Rails requests.
+ class RackMiddleware
+ CONTROLLER_KEY = 'action_controller.instance'
+
+ def initialize(app)
+ @app = app
+ end
+
+ # env - A Hash containing Rack environment details.
+ def call(env)
+ trans = transaction_from_env(env)
+ retval = nil
+
+ begin
+ retval = trans.run { @app.call(env) }
+
+ # Even in the event of an error we want to submit any metrics we
+ # might've gathered up to this point.
+ ensure
+ if env[CONTROLLER_KEY]
+ tag_controller(trans, env)
+ end
+
+ trans.finish
+ end
+
+ retval
+ end
+
+ def transaction_from_env(env)
+ trans = Transaction.new
+
+ trans.add_tag(:request_method, env['REQUEST_METHOD'])
+ trans.add_tag(:request_uri, env['REQUEST_URI'])
+
+ trans
+ end
+
+ def tag_controller(trans, env)
+ controller = env[CONTROLLER_KEY]
+ label = "#{controller.class.name}##{controller.action_name}"
+
+ trans.add_tag(:action, label)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb
new file mode 100644
index 00000000000..1ea425bc904
--- /dev/null
+++ b/lib/gitlab/metrics/sampler.rb
@@ -0,0 +1,107 @@
+module Gitlab
+ module Metrics
+ # Class that sends certain metrics to InfluxDB at a specific interval.
+ #
+ # This class is used to gather statistics that can't be directly associated
+ # with a transaction such as system memory usage, garbage collection
+ # statistics, etc.
+ class Sampler
+ # interval - The sampling interval in seconds.
+ def initialize(interval = 15)
+ @interval = interval
+ @metrics = []
+
+ @last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
+ @last_major_gc = Delta.new(GC.stat[:major_gc_count])
+
+ if Gitlab::Metrics.mri?
+ require 'allocations'
+
+ Allocations.start
+ end
+ end
+
+ def start
+ Thread.new do
+ Thread.current.abort_on_exception = true
+
+ loop do
+ sleep(@interval)
+
+ sample
+ end
+ end
+ end
+
+ def sample
+ sample_memory_usage
+ sample_file_descriptors
+ sample_objects
+ sample_gc
+
+ flush
+ ensure
+ GC::Profiler.clear
+ @metrics.clear
+ end
+
+ def flush
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
+ end
+
+ def sample_memory_usage
+ add_metric('memory_usage', value: System.memory_usage)
+ end
+
+ def sample_file_descriptors
+ add_metric('file_descriptors', value: System.file_descriptor_count)
+ end
+
+ if Metrics.mri?
+ def sample_objects
+ sample = Allocations.to_hash
+ counts = sample.each_with_object({}) do |(klass, count), hash|
+ hash[klass.name] = count
+ end
+
+ # Symbols aren't allocated so we'll need to add those manually.
+ counts['Symbol'] = Symbol.all_symbols.length
+
+ counts.each do |name, count|
+ add_metric('object_counts', { count: count }, type: name)
+ end
+ end
+ else
+ def sample_objects
+ end
+ end
+
+ def sample_gc
+ time = GC::Profiler.total_time * 1000.0
+ stats = GC.stat.merge(total_time: time)
+
+ # We want the difference of GC runs compared to the last sample, not the
+ # total amount since the process started.
+ stats[:minor_gc_count] =
+ @last_minor_gc.compared_with(stats[:minor_gc_count])
+
+ stats[:major_gc_count] =
+ @last_major_gc.compared_with(stats[:major_gc_count])
+
+ stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
+
+ add_metric('gc_statistics', stats)
+ end
+
+ def add_metric(series, values, tags = {})
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
new file mode 100644
index 00000000000..ad441decfa2
--- /dev/null
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -0,0 +1,23 @@
+module Gitlab
+ module Metrics
+ # Sidekiq middleware for tracking jobs.
+ #
+ # This middleware is intended to be used as a server-side middleware.
+ class SidekiqMiddleware
+ def call(worker, message, queue)
+ trans = Transaction.new
+
+ begin
+ trans.run { yield }
+ ensure
+ tag_worker(trans, worker)
+ trans.finish
+ end
+ end
+
+ def tag_worker(trans, worker)
+ trans.add_tag(:action, "#{worker.class.name}#perform")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
new file mode 100644
index 00000000000..7c0105d543a
--- /dev/null
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -0,0 +1,54 @@
+module Gitlab
+ module Metrics
+ module Subscribers
+ # Class for tracking the rendering timings of views.
+ class ActionView < ActiveSupport::Subscriber
+ attach_to :action_view
+
+ SERIES = 'views'
+
+ def render_template(event)
+ track(event) if current_transaction
+ end
+
+ alias_method :render_view, :render_template
+
+ private
+
+ def track(event)
+ values = values_for(event)
+ tags = tags_for(event)
+
+ current_transaction.increment(:view_duration, event.duration)
+ current_transaction.add_metric(SERIES, values, tags)
+ end
+
+ def relative_path(path)
+ path.gsub(/^#{Rails.root.to_s}\/?/, '')
+ end
+
+ def values_for(event)
+ { duration: event.duration }
+ end
+
+ def tags_for(event)
+ path = relative_path(event.payload[:identifier])
+ tags = { view: path }
+
+ file, line = Metrics.last_relative_application_frame
+
+ if file and line
+ tags[:file] = file
+ tags[:line] = line
+ end
+
+ tags
+ end
+
+ def current_transaction
+ Transaction.current
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
new file mode 100644
index 00000000000..8008b3bc895
--- /dev/null
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Metrics
+ module Subscribers
+ # Class for tracking the total query duration of a transaction.
+ class ActiveRecord < ActiveSupport::Subscriber
+ attach_to :active_record
+
+ def sql(event)
+ return unless current_transaction
+
+ current_transaction.increment(:sql_duration, event.duration)
+ end
+
+ private
+
+ def current_transaction
+ Transaction.current
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
new file mode 100644
index 00000000000..83371265278
--- /dev/null
+++ b/lib/gitlab/metrics/system.rb
@@ -0,0 +1,35 @@
+module Gitlab
+ module Metrics
+ # Module for gathering system/process statistics such as the memory usage.
+ #
+ # This module relies on the /proc filesystem being available. If /proc is
+ # not available the methods of this module will be stubbed.
+ module System
+ if File.exist?('/proc')
+ # Returns the current process' memory usage in bytes.
+ def self.memory_usage
+ mem = 0
+ match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
+
+ if match and match[1]
+ mem = match[1].to_f * 1024
+ end
+
+ mem
+ end
+
+ def self.file_descriptor_count
+ Dir.glob('/proc/self/fd/*').length
+ end
+ else
+ def self.memory_usage
+ 0.0
+ end
+
+ def self.file_descriptor_count
+ 0
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
new file mode 100644
index 00000000000..68b86de0655
--- /dev/null
+++ b/lib/gitlab/metrics/transaction.rb
@@ -0,0 +1,79 @@
+module Gitlab
+ module Metrics
+ # Class for storing metrics information of a single transaction.
+ class Transaction
+ THREAD_KEY = :_gitlab_metrics_transaction
+
+ attr_reader :uuid, :tags
+
+ def self.current
+ Thread.current[THREAD_KEY]
+ end
+
+ def initialize
+ @metrics = []
+ @uuid = SecureRandom.uuid
+
+ @started_at = nil
+ @finished_at = nil
+
+ @values = Hash.new(0)
+ @tags = {}
+ end
+
+ def duration
+ @finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
+ end
+
+ def run
+ Thread.current[THREAD_KEY] = self
+
+ @started_at = Time.now
+
+ yield
+ ensure
+ @finished_at = Time.now
+
+ Thread.current[THREAD_KEY] = nil
+ end
+
+ def add_metric(series, values, tags = {})
+ tags = tags.merge(transaction_id: @uuid)
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def increment(name, value)
+ @values[name] += value
+ end
+
+ def add_tag(key, value)
+ @tags[key] = value
+ end
+
+ def finish
+ track_self
+ submit
+ end
+
+ def track_self
+ values = { duration: duration }
+
+ @values.each do |name, value|
+ values[name] = value
+ end
+
+ add_metric('transactions', values, @tags)
+ end
+
+ def submit
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index d94b104bbf8..ba31599432b 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -62,7 +62,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].parameterize
+ 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/provider.rb b/lib/gitlab/o_auth/provider.rb
index 90c3fe8da33..9ad7a38d505 100644
--- a/lib/gitlab/o_auth/provider.rb
+++ b/lib/gitlab/o_auth/provider.rb
@@ -1,6 +1,12 @@
module Gitlab
module OAuth
class Provider
+ LABELS = {
+ "github" => "GitHub",
+ "gitlab" => "GitLab.com",
+ "google_oauth2" => "Google"
+ }.freeze
+
def self.providers
Devise.omniauth_providers
end
@@ -23,8 +29,9 @@ module Gitlab
end
def self.label_for(name)
+ name = name.to_s
config = config_for(name)
- (config && config['label']) || name.to_s.titleize
+ (config && config['label']) || LABELS[name] || name.titleize
end
end
end
diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb
new file mode 100644
index 00000000000..f33bfd0bd0e
--- /dev/null
+++ b/lib/gitlab/o_auth/session.rb
@@ -0,0 +1,17 @@
+module Gitlab
+ module OAuth
+ module Session
+ def self.create(provider, ticket)
+ Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
+ end
+
+ def self.destroy(provider, ticket)
+ Rails.cache.delete("gitlab:#{provider}:#{ticket}")
+ end
+
+ def self.valid?(provider, ticket)
+ Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index 17ce4d4b174..f1a362f5303 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -64,7 +64,7 @@ module Gitlab
# 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.downcase, ldap_person.provider)
+ 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)
else
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 0dab7bcfa4d..70de6a74e76 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -9,7 +9,7 @@ module Gitlab
else
nil
end
- @query = Shellwords.shellescape(query) if query.present?
+ @query = query
end
def objects(scope, page = nil)
@@ -20,6 +20,8 @@ module Gitlab
Kaminari.paginate_array(blobs).page(page).per(per_page)
when 'wiki_blobs'
Kaminari.paginate_array(wiki_blobs).page(page).per(per_page)
+ when 'commits'
+ Kaminari.paginate_array(commits).page(page).per(per_page)
else
super
end
@@ -27,7 +29,7 @@ module Gitlab
def total_count
@total_count ||= issues_count + merge_requests_count + blobs_count +
- notes_count + wiki_blobs_count
+ notes_count + wiki_blobs_count + commits_count
end
def blobs_count
@@ -42,6 +44,10 @@ module Gitlab
@wiki_blobs_count ||= wiki_blobs.count
end
+ def commits_count
+ @commits_count ||= commits.count
+ end
+
private
def blobs
@@ -70,6 +76,14 @@ module Gitlab
Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
end
+ def commits
+ if project.empty_repo? || query.blank?
+ []
+ else
+ project.repository.find_commits_by_message(query).compact
+ end
+ end
+
def limit_project_ids
[project.id]
end
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
index d010ade704e..4f9cdef3869 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/push_data_builder.rb
@@ -30,9 +30,12 @@ module Gitlab
# For performance purposes maximum 20 latest commits
# will be passed as post receive hook data.
- commit_attrs = commits_limited.map(&:hook_attrs)
+ commit_attrs = commits_limited.map do |commit|
+ commit.hook_attrs(with_changed_files: true)
+ end
type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push"
+
# Hash to be passed as post_receive_data
data = {
object_kind: type,
diff --git a/lib/gitlab/recaptcha.rb b/lib/gitlab/recaptcha.rb
new file mode 100644
index 00000000000..70e7f25d518
--- /dev/null
+++ b/lib/gitlab/recaptcha.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Recaptcha
+ def self.load_configurations!
+ if current_application_settings.recaptcha_enabled
+ ::Recaptcha.configure do |config|
+ config.public_key = current_application_settings.recaptcha_site_key
+ config.private_key = current_application_settings.recaptcha_private_key
+ end
+
+ true
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index da8df8a3025..be795649e59 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,73 +1,42 @@
-require 'gitlab/markdown'
+require 'banzai'
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
- class ReferenceExtractor
- attr_accessor :project, :current_user, :load_lazy_references
+ class ReferenceExtractor < Banzai::ReferenceExtractor
+ attr_accessor :project, :current_user, :author
- def initialize(project, current_user = nil, load_lazy_references: true)
+ def initialize(project, current_user = nil, author = nil)
@project = project
@current_user = current_user
- @load_lazy_references = load_lazy_references
+ @author = author
+
+ @references = {}
+
+ super()
end
- def analyze(text)
- references.clear
- @text = Gitlab::Markdown.render_without_gfm(text)
+ def analyze(text, context = {})
+ super(text, context.merge(project: project))
end
- %i(user label issue merge_request snippet commit commit_range).each do |type|
+ %i(user label merge_request snippet commit commit_range).each do |type|
define_method("#{type}s") do
- references[type]
+ @references[type] ||= references(type, reference_context)
end
end
- private
-
- def references
- @references ||= Hash.new do |references, type|
- type = type.to_sym
- next references[type] if references.has_key?(type)
-
- references[type] = pipeline_result(type)
+ def issues
+ if project && project.jira_tracker?
+ @references[:external_issue] ||= references(:external_issue, reference_context)
+ else
+ @references[:issue] ||= references(:issue, reference_context)
end
end
- # Instantiate and call HTML::Pipeline with a single reference filter type,
- # returning the result
- #
- # filter_type - Symbol reference type (e.g., :commit, :issue, etc.)
- #
- # Returns the results Array for the requested filter type
- def pipeline_result(filter_type)
- return [] if @text.blank?
-
- klass = "#{filter_type.to_s.camelize}ReferenceFilter"
- filter = Gitlab::Markdown.const_get(klass)
-
- context = {
- project: project,
- current_user: current_user,
-
- # We don't actually care about the links generated
- only_path: true,
- ignore_blockquotes: true,
-
- # ReferenceGathererFilter
- load_lazy_references: false,
- reference_filter: filter
- }
-
- pipeline = HTML::Pipeline.new([filter, Gitlab::Markdown::ReferenceGathererFilter], context)
- result = pipeline.call(@text)
-
- values = result[:references][filter_type].uniq
-
- if @load_lazy_references
- values = Gitlab::Markdown::ReferenceFilter::LazyReference.load(values).uniq
- end
+ private
- values
+ 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 9f1adc860d1..53ab2686b43 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -51,6 +51,23 @@ module Gitlab
"can contain only letters, digits, '_', '-' and '.'. "
end
+ def file_path_regex
+ @file_path_regex ||= /\A[a-zA-Z0-9_\-\.\/]*\z/.freeze
+ end
+
+ def file_path_regex_message
+ "can contain only letters, digits, '_', '-' and '.'. Separate directories with a '/'. "
+ end
+
+
+ def directory_traversal_regex
+ @directory_traversal_regex ||= /\.{2}/.freeze
+ end
+
+ def directory_traversal_regex_message
+ "cannot include directory traversal. "
+ end
+
def archive_formats_regex
# |zip|tar| tar.gz | tar.bz2 |
diff --git a/lib/gitlab/seeder.rb b/lib/gitlab/seeder.rb
index 31aa3528c4c..2ef0e982256 100644
--- a/lib/gitlab/seeder.rb
+++ b/lib/gitlab/seeder.rb
@@ -14,7 +14,7 @@ module Gitlab
def self.mute_mailer
code = <<-eos
-def Notify.delay
+def Notify.deliver_later
self
end
eos
diff --git a/lib/gitlab/sherlock.rb b/lib/gitlab/sherlock.rb
new file mode 100644
index 00000000000..6360527a7aa
--- /dev/null
+++ b/lib/gitlab/sherlock.rb
@@ -0,0 +1,19 @@
+require 'securerandom'
+
+module Gitlab
+ module Sherlock
+ @collection = Collection.new
+
+ class << self
+ attr_reader :collection
+ end
+
+ def self.enabled?
+ Rails.env.development? && !!ENV['ENABLE_SHERLOCK']
+ end
+
+ def self.enable_line_profiler?
+ RUBY_ENGINE == 'ruby'
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/collection.rb b/lib/gitlab/sherlock/collection.rb
new file mode 100644
index 00000000000..66bd6258521
--- /dev/null
+++ b/lib/gitlab/sherlock/collection.rb
@@ -0,0 +1,49 @@
+module Gitlab
+ module Sherlock
+ # A collection of transactions recorded by Sherlock.
+ #
+ # Method calls for this class are synchronized using a mutex to allow
+ # sharing of a single Collection instance between threads (e.g. when using
+ # Puma as a webserver).
+ class Collection
+ include Enumerable
+
+ def initialize
+ @transactions = []
+ @mutex = Mutex.new
+ end
+
+ def add(transaction)
+ synchronize { @transactions << transaction }
+ end
+
+ alias_method :<<, :add
+
+ def each(&block)
+ synchronize { @transactions.each(&block) }
+ end
+
+ def clear
+ synchronize { @transactions.clear }
+ end
+
+ def empty?
+ synchronize { @transactions.empty? }
+ end
+
+ def find_transaction(id)
+ find { |trans| trans.id == id }
+ end
+
+ def newest_first
+ sort { |a, b| b.finished_at <=> a.finished_at }
+ end
+
+ private
+
+ def synchronize(&block)
+ @mutex.synchronize(&block)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/file_sample.rb b/lib/gitlab/sherlock/file_sample.rb
new file mode 100644
index 00000000000..8a3e1a5e5bf
--- /dev/null
+++ b/lib/gitlab/sherlock/file_sample.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module Sherlock
+ class FileSample
+ attr_reader :id, :file, :line_samples, :events, :duration
+
+ # file - The full path to the file this sample belongs to.
+ # line_samples - An array of LineSample objects.
+ # duration - The total execution time in milliseconds.
+ # events - The total amount of events.
+ def initialize(file, line_samples, duration, events)
+ @id = SecureRandom.uuid
+ @file = file
+ @line_samples = line_samples
+ @duration = duration
+ @events = events
+ end
+
+ def relative_path
+ @relative_path ||= @file.gsub(/^#{Rails.root.to_s}\/?/, '')
+ end
+
+ def to_param
+ @id
+ end
+
+ def source
+ @source ||= File.read(@file)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/line_profiler.rb b/lib/gitlab/sherlock/line_profiler.rb
new file mode 100644
index 00000000000..aa1468bff6b
--- /dev/null
+++ b/lib/gitlab/sherlock/line_profiler.rb
@@ -0,0 +1,98 @@
+module Gitlab
+ module Sherlock
+ # Class for profiling code on a per line basis.
+ #
+ # The LineProfiler class can be used to profile code on per line basis
+ # without littering your code with Ruby implementation specific profiling
+ # methods.
+ #
+ # This profiler only includes samples taking longer than a given threshold
+ # and those that occur in the actual application (e.g. files from Gems are
+ # ignored).
+ class LineProfiler
+ # The minimum amount of time that has to be spent in a file for it to be
+ # included in a list of samples.
+ MINIMUM_DURATION = 10.0
+
+ # Profiles the given block.
+ #
+ # Example:
+ #
+ # profiler = LineProfiler.new
+ #
+ # retval, samples = profiler.profile do
+ # "cats are amazing"
+ # end
+ #
+ # retval # => "cats are amazing"
+ # samples # => [#<Gitlab::Sherlock::FileSample ...>, ...]
+ #
+ # Returns an Array containing the block's return value and an Array of
+ # FileSample objects.
+ def profile(&block)
+ if mri?
+ profile_mri(&block)
+ else
+ raise NotImplementedError,
+ 'Line profiling is not supported on this platform'
+ end
+ end
+
+ # Profiles the given block using rblineprof (MRI only).
+ def profile_mri
+ require 'rblineprof'
+
+ retval = nil
+ samples = lineprof(/^#{Rails.root.to_s}/) { retval = yield }
+
+ file_samples = aggregate_rblineprof(samples)
+
+ [retval, file_samples]
+ end
+
+ # Returns an Array of file samples based on the output of rblineprof.
+ #
+ # lineprof_stats - A Hash containing rblineprof statistics on a per file
+ # basis.
+ #
+ # Returns an Array of FileSample objects.
+ def aggregate_rblineprof(lineprof_stats)
+ samples = []
+
+ lineprof_stats.each do |(file, stats)|
+ source_lines = File.read(file).each_line.to_a
+ line_samples = []
+
+ total_duration = microsec_to_millisec(stats[0][0])
+ total_events = stats[0][2]
+
+ next if total_duration <= MINIMUM_DURATION
+
+ stats[1..-1].each_with_index do |data, index|
+ next unless source_lines[index]
+
+ duration = microsec_to_millisec(data[0])
+ events = data[2]
+
+ line_samples << LineSample.new(duration, events)
+ end
+
+ samples << FileSample.
+ new(file, line_samples, total_duration, total_events)
+ end
+
+ samples
+ end
+
+ private
+
+ def microsec_to_millisec(microsec)
+ microsec / 1000.0
+ end
+
+ def mri?
+ RUBY_ENGINE == 'ruby'
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/line_sample.rb b/lib/gitlab/sherlock/line_sample.rb
new file mode 100644
index 00000000000..eb1948eb6d6
--- /dev/null
+++ b/lib/gitlab/sherlock/line_sample.rb
@@ -0,0 +1,36 @@
+module Gitlab
+ module Sherlock
+ class LineSample
+ attr_reader :duration, :events
+
+ # duration - The execution time in milliseconds.
+ # events - The amount of events.
+ def initialize(duration, events)
+ @duration = duration
+ @events = events
+ end
+
+ # Returns the sample duration percentage relative to the given duration.
+ #
+ # Example:
+ #
+ # sample.duration # => 150
+ # sample.percentage_of(1500) # => 10.0
+ #
+ # total_duration - The total duration to compare with.
+ #
+ # Returns a float
+ def percentage_of(total_duration)
+ (duration.to_f / total_duration) * 100.0
+ end
+
+ # Returns true if the current sample takes up the majority of the given
+ # duration.
+ #
+ # total_duration - The total duration to compare with.
+ def majority_of?(total_duration)
+ percentage_of(total_duration) >= 30
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/location.rb b/lib/gitlab/sherlock/location.rb
new file mode 100644
index 00000000000..5ac265618ad
--- /dev/null
+++ b/lib/gitlab/sherlock/location.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Sherlock
+ class Location
+ attr_reader :path, :line
+
+ SHERLOCK_DIR = File.dirname(__FILE__)
+
+ # Creates a new Location from a `Thread::Backtrace::Location`.
+ def self.from_ruby_location(location)
+ new(location.path, location.lineno)
+ end
+
+ # path - The full path of the frame as a String.
+ # line - The line number of the frame as a Fixnum.
+ def initialize(path, line)
+ @path = path
+ @line = line
+ end
+
+ # Returns true if the current frame originated from the application.
+ def application?
+ @path.start_with?(Rails.root.to_s) && !path.start_with?(SHERLOCK_DIR)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/middleware.rb b/lib/gitlab/sherlock/middleware.rb
new file mode 100644
index 00000000000..687332fc5fc
--- /dev/null
+++ b/lib/gitlab/sherlock/middleware.rb
@@ -0,0 +1,41 @@
+module Gitlab
+ module Sherlock
+ # Rack middleware used for tracking request metrics.
+ class Middleware
+ CONTENT_TYPES = /text\/html|application\/json/i
+
+ IGNORE_PATHS = %r{^/sherlock}
+
+ def initialize(app)
+ @app = app
+ end
+
+ # env - A Hash containing Rack environment details.
+ def call(env)
+ if instrument?(env)
+ call_with_instrumentation(env)
+ else
+ @app.call(env)
+ end
+ end
+
+ def call_with_instrumentation(env)
+ trans = transaction_from_env(env)
+ retval = trans.run { @app.call(env) }
+
+ Sherlock.collection.add(trans)
+
+ retval
+ end
+
+ def instrument?(env)
+ !!(env['HTTP_ACCEPT'] =~ CONTENT_TYPES &&
+ env['REQUEST_URI'] !~ IGNORE_PATHS)
+ end
+
+ def transaction_from_env(env)
+ Transaction.new(env['REQUEST_METHOD'], env['REQUEST_URI'])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/query.rb b/lib/gitlab/sherlock/query.rb
new file mode 100644
index 00000000000..4917c4ae2ac
--- /dev/null
+++ b/lib/gitlab/sherlock/query.rb
@@ -0,0 +1,114 @@
+module Gitlab
+ module Sherlock
+ class Query
+ attr_reader :id, :query, :started_at, :finished_at, :backtrace
+
+ # SQL identifiers that should be prefixed with newlines.
+ PREFIX_NEWLINE = /
+ \s+(FROM
+ |(LEFT|RIGHT)?INNER\s+JOIN
+ |(LEFT|RIGHT)?OUTER\s+JOIN
+ |WHERE
+ |AND
+ |GROUP\s+BY
+ |ORDER\s+BY
+ |LIMIT
+ |OFFSET)\s+/ix # Vim indent breaks when this is on a newline :<
+
+ # Creates a new Query using a String and a separate Array of bindings.
+ #
+ # query - A String containing a SQL query, optionally with numeric
+ # placeholders (`$1`, `$2`, etc).
+ #
+ # bindings - An Array of ActiveRecord columns and their values.
+ # started_at - The start time of the query as a Time-like object.
+ # finished_at - The completion time of the query as a Time-like object.
+ #
+ # Returns a new Query object.
+ def self.new_with_bindings(query, bindings, started_at, finished_at)
+ bindings.each_with_index do |(_, value), index|
+ quoted_value = ActiveRecord::Base.connection.quote(value)
+
+ query = query.gsub("$#{index + 1}", quoted_value)
+ end
+
+ new(query, started_at, finished_at)
+ end
+
+ # query - The SQL query as a String (without placeholders).
+ # started_at - The start time of the query as a Time-like object.
+ # finished_at - The completion time of the query as a Time-like object.
+ def initialize(query, started_at, finished_at)
+ @id = SecureRandom.uuid
+ @query = query
+ @started_at = started_at
+ @finished_at = finished_at
+ @backtrace = caller_locations.map do |loc|
+ Location.from_ruby_location(loc)
+ end
+
+ unless @query.end_with?(';')
+ @query += ';'
+ end
+ end
+
+ # Returns the query duration in milliseconds.
+ def duration
+ @duration ||= (@finished_at - @started_at) * 1000.0
+ end
+
+ def to_param
+ @id
+ end
+
+ # Returns a human readable version of the query.
+ def formatted_query
+ @formatted_query ||= format_sql(@query)
+ end
+
+ # Returns the last application frame of the backtrace.
+ def last_application_frame
+ @last_application_frame ||= @backtrace.find(&:application?)
+ end
+
+ # Returns an Array of application frames (excluding Gems and the likes).
+ def application_backtrace
+ @application_backtrace ||= @backtrace.select(&:application?)
+ end
+
+ # Returns the query plan as a String.
+ def explain
+ unless @explain
+ ActiveRecord::Base.connection.transaction do
+ @explain = raw_explain(@query).values.flatten.join("\n")
+
+ # Roll back any queries that mutate data so we don't mess up
+ # anything when running explain on an INSERT, UPDATE, DELETE, etc.
+ raise ActiveRecord::Rollback
+ end
+ end
+
+ @explain
+ end
+
+ private
+
+ def raw_explain(query)
+ if Gitlab::Database.postgresql?
+ explain = "EXPLAIN ANALYZE #{query};"
+ else
+ explain = "EXPLAIN #{query};"
+ end
+
+ ActiveRecord::Base.connection.execute(explain)
+ end
+
+ def format_sql(query)
+ query.each_line.
+ map { |line| line.strip }.
+ join("\n").
+ gsub(PREFIX_NEWLINE) { "\n#{$1} " }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sherlock/transaction.rb b/lib/gitlab/sherlock/transaction.rb
new file mode 100644
index 00000000000..3489fb251b6
--- /dev/null
+++ b/lib/gitlab/sherlock/transaction.rb
@@ -0,0 +1,136 @@
+module Gitlab
+ module Sherlock
+ class Transaction
+ attr_reader :id, :type, :path, :queries, :file_samples, :started_at,
+ :finished_at, :view_counts
+
+ # type - The type of transaction (e.g. "GET", "POST", etc)
+ # path - The path of the transaction (e.g. the HTTP request path)
+ def initialize(type, path)
+ @id = SecureRandom.uuid
+ @type = type
+ @path = path
+ @queries = []
+ @file_samples = []
+ @started_at = nil
+ @finished_at = nil
+ @thread = Thread.current
+ @view_counts = Hash.new(0)
+ end
+
+ # Runs the transaction and returns the block's return value.
+ def run
+ @started_at = Time.now
+
+ retval = with_subscriptions do
+ profile_lines { yield }
+ end
+
+ @finished_at = Time.now
+
+ retval
+ end
+
+ # Returns the duration in seconds.
+ def duration
+ @duration ||= started_at && finished_at ? finished_at - started_at : 0
+ end
+
+ # Returns the total query duration in seconds.
+ def query_duration
+ @query_duration ||= @queries.map { |q| q.duration }.inject(:+) / 1000.0
+ end
+
+ def to_param
+ @id
+ end
+
+ # Returns the queries sorted in descending order by their durations.
+ def sorted_queries
+ @queries.sort { |a, b| b.duration <=> a.duration }
+ end
+
+ # Returns the file samples sorted in descending order by their durations.
+ def sorted_file_samples
+ @file_samples.sort { |a, b| b.duration <=> a.duration }
+ end
+
+ # Finds a query by the given ID.
+ #
+ # id - The query ID as a String.
+ #
+ # Returns a Query object if one could be found, nil otherwise.
+ def find_query(id)
+ @queries.find { |query| query.id == id }
+ end
+
+ # Finds a file sample by the given ID.
+ #
+ # id - The query ID as a String.
+ #
+ # Returns a FileSample object if one could be found, nil otherwise.
+ def find_file_sample(id)
+ @file_samples.find { |sample| sample.id == id }
+ end
+
+ def profile_lines
+ retval = nil
+
+ if Sherlock.enable_line_profiler?
+ retval, @file_samples = LineProfiler.new.profile { yield }
+ else
+ retval = yield
+ end
+
+ retval
+ end
+
+ def subscribe_to_active_record
+ ActiveSupport::Notifications.subscribe('sql.active_record') do |_, start, finish, _, data|
+ next unless same_thread?
+
+ track_query(data[:sql].strip, data[:binds], start, finish)
+ end
+ end
+
+ def subscribe_to_action_view
+ regex = /render_(template|partial)\.action_view/
+
+ ActiveSupport::Notifications.subscribe(regex) do |_, start, finish, _, data|
+ next unless same_thread?
+
+ track_view(data[:identifier])
+ end
+ end
+
+ private
+
+ def track_query(query, bindings, start, finish)
+ @queries << Query.new_with_bindings(query, bindings, start, finish)
+ end
+
+ def track_view(path)
+ @view_counts[path] += 1
+ end
+
+ def with_subscriptions
+ ar_subscriber = subscribe_to_active_record
+ av_subscriber = subscribe_to_action_view
+
+ retval = yield
+
+ ActiveSupport::Notifications.unsubscribe(ar_subscriber)
+ ActiveSupport::Notifications.unsubscribe(av_subscriber)
+
+ retval
+ end
+
+ # In case somebody uses a multi-threaded server locally (e.g. Puma) we
+ # _only_ want to track notifications that originate from the transaction
+ # thread.
+ def same_thread?
+ Thread.current == @thread
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
new file mode 100644
index 00000000000..1cd89b3a9c4
--- /dev/null
+++ b/lib/gitlab/sql/union.rb
@@ -0,0 +1,34 @@
+module Gitlab
+ module SQL
+ # Class for building SQL UNION statements.
+ #
+ # ORDER BYs are dropped from the relations as the final sort order is not
+ # guaranteed any way.
+ #
+ # Example usage:
+ #
+ # union = Gitlab::SQL::Union.new(user.personal_projects, user.projects)
+ # sql = union.to_sql
+ #
+ # Project.where("id IN (#{sql})")
+ class Union
+ def initialize(relations)
+ @relations = relations
+ end
+
+ def to_sql
+ # Some relations may include placeholders for prepared statements, these
+ # aren't incremented properly when joining relations together this way.
+ # By using "unprepared_statements" we remove the usage of placeholders
+ # (thus fixing this problem), at a slight performance cost.
+ fragments = ActiveRecord::Base.connection.unprepared_statement do
+ @relations.map do |rel|
+ rel.reorder(nil).to_sql
+ end
+ end
+
+ fragments.join("\nUNION\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/upgrader.rb b/lib/gitlab/upgrader.rb
index cf040971c6e..f3567f3ef85 100644
--- a/lib/gitlab/upgrader.rb
+++ b/lib/gitlab/upgrader.rb
@@ -50,15 +50,15 @@ module Gitlab
end
def fetch_git_tags
- remote_tags, _ = Gitlab::Popen.popen(%W(git ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git))
+ remote_tags, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} ls-remote --tags https://gitlab.com/gitlab-org/gitlab-ce.git))
remote_tags.split("\n").grep(/tags\/v#{current_version.major}/)
end
def update_commands
{
- "Stash changed files" => %W(git stash),
- "Get latest code" => %W(git fetch),
- "Switch to new version" => %W(git checkout v#{latest_version}),
+ "Stash changed files" => %W(#{Gitlab.config.git.bin_path} stash),
+ "Get latest code" => %W(#{Gitlab.config.git.bin_path} fetch),
+ "Switch to new version" => %W(#{Gitlab.config.git.bin_path} checkout v#{latest_version}),
"Install gems" => %W(bundle),
"Migrate DB" => %W(bundle exec rake db:migrate),
"Recompile assets" => %W(bundle exec rake assets:clean assets:precompile),
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 335dc44be19..3160a3c7582 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -51,6 +51,15 @@ module Gitlab
def allowed_fork_levels(origin_level)
[PRIVATE, INTERNAL, PUBLIC].select{ |level| level <= origin_level }
end
+
+ def level_name(level)
+ level_name = 'Unknown'
+ options.each do |name, lvl|
+ level_name = name if lvl == level.to_i
+ end
+
+ level_name
+ end
end
def private?
diff --git a/lib/omni_auth/request_forgery_protection.rb b/lib/omni_auth/request_forgery_protection.rb
index 3557522d3c9..69155131d8d 100644
--- a/lib/omni_auth/request_forgery_protection.rb
+++ b/lib/omni_auth/request_forgery_protection.rb
@@ -1,66 +1,21 @@
# Protects OmniAuth request phase against CSRF.
module OmniAuth
- # Based on ActionController::RequestForgeryProtection.
- class RequestForgeryProtection
- def initialize(env)
- @env = env
- end
-
- def request
- @request ||= ActionDispatch::Request.new(@env)
- end
-
- def session
- request.session
- end
-
- def reset_session
- request.reset_session
- end
-
- def params
- request.params
- end
-
- def call
- verify_authenticity_token
- end
+ module RequestForgeryProtection
+ class Controller < ActionController::Base
+ protect_from_forgery with: :exception
- def verify_authenticity_token
- if !verified_request?
- Rails.logger.warn "Can't verify CSRF token authenticity" if Rails.logger
- handle_unverified_request
+ def index
+ head :ok
end
end
- private
-
- def protect_against_forgery?
- ApplicationController.allow_forgery_protection
- end
-
- def request_forgery_protection_token
- ApplicationController.request_forgery_protection_token
- end
-
- def forgery_protection_strategy
- ApplicationController.forgery_protection_strategy
- end
-
- def verified_request?
- !protect_against_forgery? || request.get? || request.head? ||
- form_authenticity_token == params[request_forgery_protection_token] ||
- form_authenticity_token == request.headers['X-CSRF-Token']
- end
-
- def handle_unverified_request
- forgery_protection_strategy.new(self).handle_unverified_request
+ def self.app
+ @app ||= Controller.action(:index)
end
- # Sets the token value for the current session.
- def form_authenticity_token
- session[:_csrf_token] ||= SecureRandom.base64(32)
+ def self.call(env)
+ app.call(env)
end
end
end
diff --git a/lib/rouge/formatters/html_gitlab.rb b/lib/rouge/formatters/html_gitlab.rb
index 6762ca47c32..8c309efc7b8 100644
--- a/lib/rouge/formatters/html_gitlab.rb
+++ b/lib/rouge/formatters/html_gitlab.rb
@@ -39,7 +39,7 @@ module Rouge
lineanchorsid: 'L',
anchorlinenos: false,
inline_theme: nil
- )
+ )
@nowrap = nowrap
@cssclass = cssclass
@linenos = linenos
diff --git a/lib/support/init.d/gitlab b/lib/support/init.d/gitlab
index a80e7e77430..c5f07c8b508 100755
--- a/lib/support/init.d/gitlab
+++ b/lib/support/init.d/gitlab
@@ -33,14 +33,14 @@ app_user="git"
app_root="/home/$app_user/gitlab"
pid_path="$app_root/tmp/pids"
socket_path="$app_root/tmp/sockets"
+rails_socket="$socket_path/gitlab.socket"
web_server_pid_path="$pid_path/unicorn.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
mail_room_enabled=false
mail_room_pid_path="$pid_path/mail_room.pid"
-gitlab_git_http_server_pid_path="$pid_path/gitlab-git-http-server.pid"
-gitlab_git_http_server_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-git-http-server.socket -authBackend http://127.0.0.1:8080"
-gitlab_git_http_server_repo_root='/home/git/repositories'
-gitlab_git_http_server_log="$app_root/log/gitlab-git-http-server.log"
+gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
+gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $rails_socket -documentRoot $app_root/public"
+gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
shell_path="/bin/bash"
# Read configuration variable file if it is present
@@ -76,8 +76,8 @@ check_pids(){
else
spid=0
fi
- if [ -f "$gitlab_git_http_server_pid_path" ]; then
- hpid=$(cat "$gitlab_git_http_server_pid_path")
+ if [ -f "$gitlab_workhorse_pid_path" ]; then
+ hpid=$(cat "$gitlab_workhorse_pid_path")
else
hpid=0
fi
@@ -92,9 +92,9 @@ check_pids(){
## Called when we have started the two processes and are waiting for their pid files.
wait_for_pids(){
- # We are sleeping a bit here mostly because sidekiq is slow at writing it's pid
+ # We are sleeping a bit here mostly because sidekiq is slow at writing its pid
i=0;
- while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_git_http_server_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
+ while [ ! -f $web_server_pid_path ] || [ ! -f $sidekiq_pid_path ] || [ ! -f $gitlab_workhorse_pid_path ] || { [ "$mail_room_enabled" = true ] && [ ! -f $mail_room_pid_path ]; }; do
sleep 0.1;
i=$((i+1))
if [ $((i%10)) = 0 ]; then
@@ -108,7 +108,7 @@ wait_for_pids(){
}
# We use the pids in so many parts of the script it makes sense to always check them.
-# Only after start() is run should the pids change. Sidekiq sets it's own pid.
+# Only after start() is run should the pids change. Sidekiq sets its own pid.
check_pids
@@ -131,9 +131,9 @@ check_status(){
fi
if [ $hpid -ne 0 ]; then
kill -0 "$hpid" 2>/dev/null
- gitlab_git_http_server_status="$?"
+ gitlab_workhorse_status="$?"
else
- gitlab_git_http_server_status="-1"
+ gitlab_workhorse_status="-1"
fi
if [ "$mail_room_enabled" = true ]; then
if [ $mpid -ne 0 ]; then
@@ -143,7 +143,7 @@ check_status(){
mail_room_status="-1"
fi
fi
- if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_git_http_server_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
+ if [ $web_status = 0 ] && [ $sidekiq_status = 0 ] && [ $gitlab_workhorse_status = 0 ] && { [ "$mail_room_enabled" != true ] || [ $mail_room_status = 0 ]; }; then
gitlab_status=0
else
# http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
@@ -171,9 +171,9 @@ check_stale_pids(){
exit 1
fi
fi
- if [ "$hpid" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ]; then
- echo "Removing stale gitlab-git-http-server pid. This is most likely caused by gitlab-git-http-server crashing the last time it ran."
- if ! rm "$gitlab_git_http_server_pid_path"; then
+ if [ "$hpid" != "0" ] && [ "$gitlab_workhorse_status" != "0" ]; then
+ echo "Removing stale gitlab-workhorse pid. This is most likely caused by gitlab-workhorse crashing the last time it ran."
+ if ! rm "$gitlab_workhorse_pid_path"; then
echo "Unable to remove stale pid, exiting"
exit 1
fi
@@ -190,7 +190,7 @@ check_stale_pids(){
## If no parts of the service is running, bail out.
exit_if_not_running(){
check_stale_pids
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
echo "GitLab is not running."
exit
fi
@@ -206,8 +206,8 @@ start_gitlab() {
if [ "$sidekiq_status" != "0" ]; then
echo "Starting GitLab Sidekiq"
fi
- if [ "$gitlab_git_http_server_status" != "0" ]; then
- echo "Starting gitlab-git-http-server"
+ if [ "$gitlab_workhorse_status" != "0" ]; then
+ echo "Starting gitlab-workhorse"
fi
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" != "0" ]; then
echo "Starting GitLab MailRoom"
@@ -230,15 +230,14 @@ start_gitlab() {
RAILS_ENV=$RAILS_ENV bin/background_jobs start &
fi
- if [ "$gitlab_git_http_server_status" = "0" ]; then
- echo "The gitlab-git-http-server is already running with pid $spid, not restarting"
+ if [ "$gitlab_workhorse_status" = "0" ]; then
+ echo "The gitlab-workhorse is already running with pid $spid, not restarting"
else
- # No need to remove a socket, gitlab-git-http-server does this itself
- $app_root/bin/daemon_with_pidfile $gitlab_git_http_server_pid_path \
- $app_root/../gitlab-git-http-server/gitlab-git-http-server \
- $gitlab_git_http_server_options \
- $gitlab_git_http_server_repo_root \
- >> $gitlab_git_http_server_log 2>&1 &
+ # No need to remove a socket, gitlab-workhorse does this itself
+ $app_root/bin/daemon_with_pidfile $gitlab_workhorse_pid_path \
+ $app_root/../gitlab-workhorse/gitlab-workhorse \
+ $gitlab_workhorse_options \
+ >> $gitlab_workhorse_log 2>&1 &
fi
if [ "$mail_room_enabled" = true ]; then
@@ -268,9 +267,9 @@ stop_gitlab() {
echo "Shutting down GitLab Sidekiq"
RAILS_ENV=$RAILS_ENV bin/background_jobs stop
fi
- if [ "$gitlab_git_http_server_status" = "0" ]; then
- echo "Shutting down gitlab-git-http-server"
- kill -- $(cat $gitlab_git_http_server_pid_path)
+ if [ "$gitlab_workhorse_status" = "0" ]; then
+ echo "Shutting down gitlab-workhorse"
+ kill -- $(cat $gitlab_workhorse_pid_path)
fi
if [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; then
echo "Shutting down GitLab MailRoom"
@@ -278,11 +277,11 @@ stop_gitlab() {
fi
# If something needs to be stopped, lets wait for it to stop. Never use SIGKILL in a script.
- while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_git_http_server_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
+ while [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse_status" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; do
sleep 1
check_status
printf "."
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
printf "\n"
break
fi
@@ -291,8 +290,8 @@ stop_gitlab() {
sleep 1
# Cleaning up unused pids
rm "$web_server_pid_path" 2>/dev/null
- # rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up it's own pid.
- rm -f "$gitlab_git_http_server_pid_path"
+ # rm "$sidekiq_pid_path" 2>/dev/null # Sidekiq seems to be cleaning up its own pid.
+ rm -f "$gitlab_workhorse_pid_path"
if [ "$mail_room_enabled" = true ]; then
rm "$mail_room_pid_path" 2>/dev/null
fi
@@ -300,10 +299,10 @@ stop_gitlab() {
print_status
}
-## Prints the status of GitLab and it's components.
+## Prints the status of GitLab and its components.
print_status() {
check_status
- if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_git_http_server_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
+ if [ "$web_status" != "0" ] && [ "$sidekiq_status" != "0" ] && [ "$gitlab_workhorse_status" != "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" != "0" ]; }; then
echo "GitLab is not running."
return
fi
@@ -317,10 +316,10 @@ print_status() {
else
printf "The GitLab Sidekiq job dispatcher is \033[31mnot running\033[0m.\n"
fi
- if [ "$gitlab_git_http_server_status" = "0" ]; then
- echo "The gitlab-git-http-server with pid $hpid is running."
+ if [ "$gitlab_workhorse_status" = "0" ]; then
+ echo "The gitlab-workhorse with pid $hpid is running."
else
- printf "The gitlab-git-http-server is \033[31mnot running\033[0m.\n"
+ printf "The gitlab-workhorse is \033[31mnot running\033[0m.\n"
fi
if [ "$mail_room_enabled" = true ]; then
if [ "$mail_room_status" = "0" ]; then
@@ -329,12 +328,12 @@ print_status() {
printf "The GitLab MailRoom email processor is \033[31mnot running\033[0m.\n"
fi
fi
- if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] && [ "$sidekiq_status" = "0" ] && [ "$gitlab_workhorse_status" = "0" ] && { [ "$mail_room_enabled" != true ] || [ "$mail_room_status" = "0" ]; }; then
printf "GitLab and all its components are \033[32mup and running\033[0m.\n"
fi
}
-## Tells unicorn to reload it's config and Sidekiq to restart
+## Tells unicorn to reload its config and Sidekiq to restart
reload_gitlab(){
exit_if_not_running
if [ "$wpid" = "0" ];then
@@ -360,7 +359,7 @@ reload_gitlab(){
## Restarts Sidekiq and Unicorn.
restart_gitlab(){
check_status
- if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_git_http_server" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
+ if [ "$web_status" = "0" ] || [ "$sidekiq_status" = "0" ] || [ "$gitlab_workhorse" = "0" ] || { [ "$mail_room_enabled" = true ] && [ "$mail_room_status" = "0" ]; }; then
stop_gitlab
fi
start_gitlab
diff --git a/lib/support/init.d/gitlab.default.example b/lib/support/init.d/gitlab.default.example
index aab5acaa72c..1937ca582b0 100755
--- a/lib/support/init.d/gitlab.default.example
+++ b/lib/support/init.d/gitlab.default.example
@@ -9,11 +9,11 @@ RAILS_ENV="production"
# The default is "git".
app_user="git"
-# app_root defines the folder in which gitlab and it's components are installed.
+# app_root defines the folder in which gitlab and its components are installed.
# The default is "/home/$app_user/gitlab"
app_root="/home/$app_user/gitlab"
-# pid_path defines a folder in which the gitlab and it's components place their pids.
+# pid_path defines a folder in which the gitlab and its components place their pids.
# This variable is also used below to define the relevant pids for the gitlab components.
# The default is "$app_root/tmp/pids"
pid_path="$app_root/tmp/pids"
@@ -30,15 +30,14 @@ web_server_pid_path="$pid_path/unicorn.pid"
# The default is "$pid_path/sidekiq.pid"
sidekiq_pid_path="$pid_path/sidekiq.pid"
-gitlab_git_http_server_pid_path="$pid_path/gitlab-git-http-server.pid"
-# The -listenXxx settings determine where gitlab-git-http-server
+gitlab_workhorse_pid_path="$pid_path/gitlab-workhorse.pid"
+# The -listenXxx settings determine where gitlab-workhorse
# listens for connections from NGINX. To listen on localhost:8181, write
# '-listenNetwork tcp -listenAddr localhost:8181'.
-# The -authBackend setting tells gitlab-git-http-server where it can reach
+# The -authBackend setting tells gitlab-workhorse where it can reach
# Unicorn.
-gitlab_git_http_server_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-git-http-server.socket -authBackend http://127.0.0.1:8080"
-gitlab_git_http_server_repo_root="/home/git/repositories"
-gitlab_git_http_server_log="$app_root/log/gitlab-git-http-server.log"
+gitlab_workhorse_options="-listenUmask 0 -listenNetwork unix -listenAddr $socket_path/gitlab-workhorse.socket -authBackend http://127.0.0.1:8080 -authSocket $socket_path/gitlab.socket -documentRoot $app_root/public"
+gitlab_workhorse_log="$app_root/log/gitlab-workhorse.log"
# mail_room_enabled specifies whether mail_room, which is used to process incoming email, is enabled.
# This is required for the Reply by email feature.
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 1e55c5a0486..fc5475c4eef 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -10,41 +10,19 @@
## If you change this file in a Merge Request, please also create
## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
##
-##################################
-## CHUNKED TRANSFER ##
-##################################
-##
-## It is a known issue that Git-over-HTTP requires chunked transfer encoding [0]
-## which is not supported by Nginx < 1.3.9 [1]. As a result, pushing a large object
-## with Git (i.e. a single large file) can lead to a 411 error. In theory you can get
-## around this by tweaking this configuration file and either:
-## - installing an old version of Nginx with the chunkin module [2] compiled in, or
-## - using a newer version of Nginx.
-##
-## At the time of writing we do not know if either of these theoretical solutions works.
-## As a workaround users can use Git over SSH to push large files.
-##
-## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99
-## [1] https://github.com/agentzh/chunkin-nginx-module#status
-## [2] https://github.com/agentzh/chunkin-nginx-module
-##
###################################
## configuration ##
###################################
##
## See installation.md#using-https for additional HTTPS configuration details.
-upstream gitlab {
- server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0;
-}
-
-upstream gitlab-git-http-server {
- server unix:/home/git/gitlab/tmp/sockets/gitlab-git-http-server.socket fail_timeout=0;
+upstream gitlab-workhorse {
+ server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
## Normal HTTP host
server {
- ## Either remove "default_server" from the listen line below,
+ ## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server;
@@ -54,10 +32,6 @@ server {
server_tokens off; ## Don't show the nginx version number, a security best practice
root /home/git/gitlab/public;
- ## Increase this if you want to upload large attachments
- ## Or if you want to accept large git objects over http
- client_max_body_size 20m;
-
## See app/controllers/application_controller.rb for headers set
## Individual nginx logs for this GitLab vhost
@@ -65,16 +39,8 @@ server {
error_log /var/log/nginx/gitlab_error.log;
location / {
- ## Serve static files from defined root folder.
- ## @gitlab is a named location for the upstream fallback, see below.
- try_files $uri $uri/index.html $uri.html @gitlab;
- }
-
- ## We route uploads through GitLab to prevent XSS and enforce access control.
- location /uploads/ {
- ## If you use HTTPS make sure you disable gzip compression
- ## to be safe against BREACH attack.
- # gzip off;
+ client_max_body_size 0;
+ gzip off;
## https://github.com/gitlabhq/gitlabhq/issues/694
## Some requests take more than 30 seconds.
@@ -82,94 +48,13 @@ server {
proxy_connect_timeout 300;
proxy_redirect off;
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Frame-Options SAMEORIGIN;
-
- proxy_pass http://gitlab;
- }
-
- ## If a file, which is not found in the root folder is requested,
- ## then the proxy passes the request to the upsteam (gitlab unicorn).
- location @gitlab {
- ## If you use HTTPS make sure you disable gzip compression
- ## to be safe against BREACH attack.
- # gzip off;
-
- ## https://github.com/gitlabhq/gitlabhq/issues/694
- ## Some requests take more than 30 seconds.
- proxy_read_timeout 300;
- proxy_connect_timeout 300;
- proxy_redirect off;
+ proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Frame-Options SAMEORIGIN;
- proxy_pass http://gitlab;
+ proxy_pass http://gitlab-workhorse;
}
-
- location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
- # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block
- error_page 418 = @gitlab-git-http-server;
- return 418;
- }
-
- location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive {
- # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block
- error_page 418 = @gitlab-git-http-server;
- return 418;
- }
-
- location ~ ^/api/v3/projects/.*/repository/archive {
- # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block
- error_page 418 = @gitlab-git-http-server;
- return 418;
- }
-
- location @gitlab-git-http-server {
- ## If you use HTTPS make sure you disable gzip compression
- ## to be safe against BREACH attack.
- # gzip off;
-
- ## https://github.com/gitlabhq/gitlabhq/issues/694
- ## Some requests take more than 30 seconds.
- proxy_read_timeout 300;
- proxy_connect_timeout 300;
- proxy_redirect off;
-
- # Do not buffer Git HTTP responses
- proxy_buffering off;
-
- # The following settings only work with NGINX 1.7.11 or newer
- #
- # # Pass chunked request bodies to gitlab-git-http-server as-is
- # proxy_request_buffering off;
- # proxy_http_version 1.1;
-
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
-
- proxy_pass http://gitlab-git-http-server;
- }
-
- ## Enable gzip compression as per rails guide:
- ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression
- ## WARNING: If you are using relative urls remove the block below
- ## See config/application.rb under "Relative url support" for the list of
- ## other files that need to be changed for relative url support
- location ~ ^/(assets)/ {
- root /home/git/gitlab/public;
- gzip_static on; # to serve pre-gzipped version
- expires max;
- add_header Cache-Control public;
- }
-
- error_page 502 /502.html;
}
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 08641bbcc17..1e5f85413ec 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -14,41 +14,19 @@
## If you change this file in a Merge Request, please also create
## a Merge Request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
##
-##################################
-## CHUNKED TRANSFER ##
-##################################
-##
-## It is a known issue that Git-over-HTTP requires chunked transfer encoding [0]
-## which is not supported by Nginx < 1.3.9 [1]. As a result, pushing a large object
-## with Git (i.e. a single large file) can lead to a 411 error. In theory you can get
-## around this by tweaking this configuration file and either:
-## - installing an old version of Nginx with the chunkin module [2] compiled in, or
-## - using a newer version of Nginx.
-##
-## At the time of writing we do not know if either of these theoretical solutions works.
-## As a workaround users can use Git over SSH to push large files.
-##
-## [0] https://git.kernel.org/cgit/git/git.git/tree/Documentation/technical/http-protocol.txt#n99
-## [1] https://github.com/agentzh/chunkin-nginx-module#status
-## [2] https://github.com/agentzh/chunkin-nginx-module
-##
###################################
## configuration ##
###################################
##
## See installation.md#using-https for additional HTTPS configuration details.
-upstream gitlab {
- server unix:/home/git/gitlab/tmp/sockets/gitlab.socket fail_timeout=0;
-}
-
-upstream gitlab-git-http-server {
- server unix:/home/git/gitlab/tmp/sockets/gitlab-git-http-server.socket fail_timeout=0;
+upstream gitlab-workhorse {
+ server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
}
## Redirects all HTTP traffic to the HTTPS host
server {
- ## Either remove "default_server" from the listen line below,
+ ## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)
@@ -56,12 +34,11 @@ server {
listen [::]:80 ipv6only=on default_server;
server_name YOUR_SERVER_FQDN; ## Replace this with something like gitlab.example.com
server_tokens off; ## Don't show the nginx version number, a security best practice
- return 301 https://$server_name$request_uri;
+ return 301 https://$http_host$request_uri;
access_log /var/log/nginx/gitlab_access.log;
error_log /var/log/nginx/gitlab_error.log;
}
-
## HTTPS host
server {
listen 0.0.0.0:443 ssl;
@@ -70,10 +47,6 @@ server {
server_tokens off; ## Don't show the nginx version number, a security best practice
root /home/git/gitlab/public;
- ## Increase this if you want to upload large attachments
- ## Or if you want to accept large git objects over http
- client_max_body_size 20m;
-
## Strong SSL Security
## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html & https://cipherli.st/
ssl on;
@@ -110,38 +83,7 @@ server {
error_log /var/log/nginx/gitlab_error.log;
location / {
- ## Serve static files from defined root folder.
- ## @gitlab is a named location for the upstream fallback, see below.
- try_files $uri $uri/index.html $uri.html @gitlab;
- }
-
- ## We route uploads through GitLab to prevent XSS and enforce access control.
- location /uploads/ {
- ## If you use HTTPS make sure you disable gzip compression
- ## to be safe against BREACH attack.
- gzip off;
-
- ## https://github.com/gitlabhq/gitlabhq/issues/694
- ## Some requests take more than 30 seconds.
- proxy_read_timeout 300;
- proxy_connect_timeout 300;
- proxy_redirect off;
-
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-Ssl on;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Frame-Options SAMEORIGIN;
-
- proxy_pass http://gitlab;
- }
-
- ## If a file, which is not found in the root folder is requested,
- ## then the proxy passes the request to the upsteam (gitlab unicorn).
- location @gitlab {
- ## If you use HTTPS make sure you disable gzip compression
- ## to be safe against BREACH attack.
+ client_max_body_size 0;
gzip off;
## https://github.com/gitlabhq/gitlabhq/issues/694
@@ -150,73 +92,13 @@ server {
proxy_connect_timeout 300;
proxy_redirect off;
- proxy_set_header Host $http_host;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Forwarded-Ssl on;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
- proxy_set_header X-Forwarded-Proto $scheme;
- proxy_set_header X-Frame-Options SAMEORIGIN;
-
- proxy_pass http://gitlab;
- }
-
- location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
- # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block
- error_page 418 = @gitlab-git-http-server;
- return 418;
- }
-
- location ~ ^/[\w\.-]+/[\w\.-]+/repository/archive {
- # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block
- error_page 418 = @gitlab-git-http-server;
- return 418;
- }
-
- location ~ ^/api/v3/projects/.*/repository/archive {
- # 'Error' 418 is a hack to re-use the @gitlab-git-http-server block
- error_page 418 = @gitlab-git-http-server;
- return 418;
- }
-
- location @gitlab-git-http-server {
- ## If you use HTTPS make sure you disable gzip compression
- ## to be safe against BREACH attack.
- gzip off;
-
- ## https://github.com/gitlabhq/gitlabhq/issues/694
- ## Some requests take more than 30 seconds.
- proxy_read_timeout 300;
- proxy_connect_timeout 300;
- proxy_redirect off;
-
- # Do not buffer Git HTTP responses
- proxy_buffering off;
-
- # The following settings only work with NGINX 1.7.11 or newer
- #
- # # Pass chunked request bodies to gitlab-git-http-server as-is
- # proxy_request_buffering off;
- # proxy_http_version 1.1;
+ proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Ssl on;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
- proxy_pass http://gitlab-git-http-server;
+ proxy_pass http://gitlab-workhorse;
}
-
- ## Enable gzip compression as per rails guide:
- ## http://guides.rubyonrails.org/asset_pipeline.html#gzip-compression
- ## WARNING: If you are using relative urls remove the block below
- ## See config/application.rb under "Relative url support" for the list of
- ## other files that need to be changed for relative url support
- location ~ ^/(assets)/ {
- root /home/git/gitlab/public;
- gzip_static on; # to serve pre-gzipped version
- expires max;
- add_header Cache-Control public;
- }
-
- error_page 502 /502.html;
}
diff --git a/lib/tasks/ci/schedule_builds.rake b/lib/tasks/ci/schedule_builds.rake
deleted file mode 100644
index 49435504c67..00000000000
--- a/lib/tasks/ci/schedule_builds.rake
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace :ci do
- desc "GitLab CI | Clean running builds"
- task schedule_builds: :environment do
- Ci::Scheduler.new.perform
- end
-end
diff --git a/lib/tasks/flay.rake b/lib/tasks/flay.rake
new file mode 100644
index 00000000000..e9587595fef
--- /dev/null
+++ b/lib/tasks/flay.rake
@@ -0,0 +1,9 @@
+desc 'Code duplication analyze via flay'
+task :flay do
+ output = %x(bundle exec flay --mass 35 app/ lib/gitlab/)
+
+ if output.include? "Similar code found"
+ puts output
+ exit 1
+ end
+end
diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake
new file mode 100644
index 00000000000..3bfe999ae74
--- /dev/null
+++ b/lib/tasks/flog.rake
@@ -0,0 +1,25 @@
+desc 'Code complexity analyze via flog'
+task :flog do
+ output = %x(bundle exec flog -m app/ lib/gitlab)
+ exit_code = 0
+ minimum_score = 70
+ output = output.lines
+
+ # Skip total complexity score
+ output.shift
+
+ # Skip some trash info
+ output.shift
+
+ output.each do |line|
+ score, method = line.split(" ")
+ score = score.to_i
+
+ if score > minimum_score
+ exit_code = 1
+ puts "High complexity in #{method}. Score: #{score}"
+ end
+ end
+
+ exit exit_code
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index f20c7f71ba5..cb4abe13799 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -12,6 +12,8 @@ namespace :gitlab do
Rake::Task["gitlab:backup:repo:create"].invoke
Rake::Task["gitlab:backup:uploads:create"].invoke
Rake::Task["gitlab:backup:builds:create"].invoke
+ Rake::Task["gitlab:backup:artifacts:create"].invoke
+ Rake::Task["gitlab:backup:lfs:create"].invoke
backup = Backup::Manager.new
backup.pack
@@ -32,6 +34,8 @@ namespace :gitlab do
Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
+ Rake::Task["gitlab:backup:artifacts:restore"].invoke unless backup.skipped?("artifacts")
+ Rake::Task["gitlab:backup:lfs:restore"].invoke unless backup.skipped?("lfs")
Rake::Task["gitlab:shell:setup"].invoke
backup.cleanup
@@ -113,6 +117,44 @@ namespace :gitlab do
end
end
+ namespace :artifacts do
+ task create: :environment do
+ $progress.puts "Dumping artifacts ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("artifacts")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Artifacts.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring artifacts ... ".blue
+ Backup::Artifacts.new.restore
+ $progress.puts "done".green
+ end
+ end
+
+ namespace :lfs do
+ task create: :environment do
+ $progress.puts "Dumping lfs objects ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("lfs")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Lfs.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring lfs objects ... ".blue
+ Backup::Lfs.new.restore
+ $progress.puts "done".green
+ end
+ end
+
def configure_cron_mode
if ENV['CRON']
# We need an object we can say 'puts' and 'print' to; let's use a
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 2e73f792a9d..0469c5a61c3 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -331,7 +331,7 @@ namespace :gitlab do
end
def check_redis_version
- min_redis_version = "2.4.0"
+ min_redis_version = "2.8.0"
print "Redis version >= #{min_redis_version}? ... "
redis_version = run(%W(redis-cli --version))
@@ -822,10 +822,27 @@ namespace :gitlab do
namespace_dirs.each do |namespace_dir|
repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
- repo_dirs.each do |dir|
- puts "\nChecking repo at #{dir}"
- system(*%w(git fsck), chdir: dir)
- end
+ repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
+ end
+ end
+ end
+
+ 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)
+ 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.path_with_namespace}.git"
+ )
+ end
+
+ repo_dirs.each { |repo_dir| check_repo_integrity(repo_dir) }
+ else
+ puts "\nUser '#{username}' not found".red
end
end
end
@@ -952,4 +969,35 @@ namespace :gitlab do
false
end
end
+
+ def check_repo_integrity(repo_dir)
+ puts "\nChecking repo at #{repo_dir.yellow}"
+
+ git_fsck(repo_dir)
+ check_config_lock(repo_dir)
+ check_ref_locks(repo_dir)
+ end
+
+ def git_fsck(repo_dir)
+ puts "Running `git fsck`".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}"
+ 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
+ lock_files.each do |lock_file|
+ puts " #{lock_file}"
+ end
+ else
+ puts "No ref lock files exist".green
+ end
+ end
end
diff --git a/lib/tasks/gitlab/git.rake b/lib/tasks/gitlab/git.rake
new file mode 100644
index 00000000000..65ee430d550
--- /dev/null
+++ b/lib/tasks/gitlab/git.rake
@@ -0,0 +1,55 @@
+namespace :gitlab do
+ namespace :git do
+
+ desc "GitLab | Git | Repack"
+ task repack: :environment do
+ failures = perform_git_cmd(%W(git repack -a --quiet), "Repacking repo")
+ if failures.empty?
+ puts "Done".green
+ else
+ output_failures(failures)
+ end
+ end
+
+ desc "GitLab | Git | Run garbage collection on all repos"
+ task gc: :environment do
+ failures = perform_git_cmd(%W(git gc --auto --quiet), "Garbage Collecting")
+ if failures.empty?
+ puts "Done".green
+ else
+ output_failures(failures)
+ end
+ end
+
+ desc "GitLab | Git | Prune all repos"
+ task prune: :environment do
+ failures = perform_git_cmd(%W(git prune), "Git Prune")
+ if failures.empty?
+ puts "Done".green
+ else
+ output_failures(failures)
+ end
+ end
+
+ def perform_git_cmd(cmd, message)
+ puts "Starting #{message} on all repositories"
+
+ failures = []
+ all_repos do |repo|
+ if system(*cmd, chdir: repo)
+ puts "Performed #{message} at #{repo}"
+ else
+ failures << repo
+ end
+ end
+
+ failures
+ end
+
+ def output_failures(failures)
+ puts "The following repositories reported errors:".red
+ failures.each { |f| puts "- #{f}" }
+ end
+
+ end
+end
diff --git a/lib/tasks/gitlab/import.rake b/lib/tasks/gitlab/import.rake
index c1ee271ae2b..1c04f47f08f 100644
--- a/lib/tasks/gitlab/import.rake
+++ b/lib/tasks/gitlab/import.rake
@@ -64,6 +64,8 @@ namespace :gitlab do
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
diff --git a/lib/tasks/gitlab/list_repos.rake b/lib/tasks/gitlab/list_repos.rake
new file mode 100644
index 00000000000..c7596e7abcb
--- /dev/null
+++ b/lib/tasks/gitlab/list_repos.rake
@@ -0,0 +1,17 @@
+namespace :gitlab do
+ task list_repos: :environment do
+ scope = Project
+ if ENV['SINCE']
+ date = Time.parse(ENV['SINCE'])
+ warn "Listing repositories with activity or changes since #{date}"
+ project_ids = Project.where('last_activity_at > ? OR updated_at > ?', date, date).pluck(:id).sort
+ namespace_ids = Namespace.where(['updated_at > ?', date]).pluck(:id).sort
+ 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)
+ puts base + '.git'
+ puts base + '.wiki.git'
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 3c0cc763d17..dd61632e557 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -17,7 +17,7 @@ namespace :gitlab do
# Clone if needed
unless File.directory?(target_dir)
- system(*%W(git clone -- #{args.repo} #{target_dir}))
+ system(*%W(#{Gitlab.config.git.bin_path} clone -- #{args.repo} #{target_dir}))
end
# Make sure we're on the right tag
@@ -27,7 +27,7 @@ namespace :gitlab do
reseted = reset_to_commit(args)
unless reseted
- system(*%W(git fetch origin))
+ system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
reset_to_commit(args)
end
@@ -128,14 +128,14 @@ namespace :gitlab do
end
def reset_to_commit(args)
- tag, status = Gitlab::Popen.popen(%W(git describe -- #{args.tag}))
+ tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- #{args.tag}))
unless status.zero?
- tag, status = Gitlab::Popen.popen(%W(git describe -- origin/#{args.tag}))
+ tag, status = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} describe -- origin/#{args.tag}))
end
tag = tag.strip
- system(*%W(git reset --hard #{tag}))
+ 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 c95b6540ebc..ebe516ec879 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -2,16 +2,6 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end
end
-unless STDOUT.isatty
- module Colored
- extend self
-
- def colorize(string, options={})
- string
- end
- end
-end
-
namespace :gitlab do
# Ask if the user wants to continue
@@ -103,7 +93,7 @@ namespace :gitlab do
gitlab_user = Gitlab.config.gitlab.user
current_user = run(%W(whoami)).chomp
unless current_user == gitlab_user
- puts "#{Colored.color(:black)+Colored.color(:on_yellow)} Warning #{Colored.extra(:clear)}"
+ puts " Warning ".colorize(:black).on_yellow
puts " You are running as user #{current_user.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}."
@@ -128,4 +118,12 @@ namespace :gitlab do
false
end
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
+ end
+ end
+ end
end
diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake
new file mode 100644
index 00000000000..9980e0b7984
--- /dev/null
+++ b/lib/tasks/grape.rake
@@ -0,0 +1,8 @@
+namespace :grape do
+ desc 'Print compiled grape routes'
+ task routes: :environment do
+ API::API.routes.each do |route|
+ puts route
+ end
+ end
+end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 365ff2defd4..0985ef3a669 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -19,6 +19,33 @@ namespace :spec do
run_commands(cmds)
end
+ desc 'GitLab | Rspec | Run model specs'
+ task :models do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(rspec spec --tag @models)
+ ]
+ run_commands(cmds)
+ end
+
+ desc 'GitLab | Rspec | Run service specs'
+ task :services do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(rspec spec --tag @services)
+ ]
+ run_commands(cmds)
+ end
+
+ desc 'GitLab | Rspec | Run lib specs'
+ task :lib do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(rspec spec --tag @lib)
+ ]
+ run_commands(cmds)
+ end
+
desc 'GitLab | Rspec | Run benchmark specs'
task :benchmark do
cmds = [
@@ -32,7 +59,7 @@ namespace :spec do
task :other do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature --tag ~@benchmark)
+ %W(rspec spec --tag ~@api --tag ~@feature --tag ~@models --tag ~@lib --tag ~@services --tag ~@benchmark)
]
run_commands(cmds)
end
diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake
index c8881be0954..3acfc6e2075 100644
--- a/lib/tasks/spinach.rake
+++ b/lib/tasks/spinach.rake
@@ -1,6 +1,26 @@
Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach')
namespace :spinach do
+ namespace :project do
+ desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features"
+ task :half do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(spinach --tags @project_commits,@project_issues,@project_merge_requests),
+ ]
+ run_commands(cmds)
+ end
+
+ desc "GitLab | Spinach | Run remaining project spinach features"
+ task :rest do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(spinach --tags ~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests),
+ ]
+ run_commands(cmds)
+ end
+ end
+
desc "GitLab | Spinach | Run project spinach features"
task :project do
cmds = [
diff --git a/lib/uploaded_file.rb b/lib/uploaded_file.rb
new file mode 100644
index 00000000000..d4291f012d3
--- /dev/null
+++ b/lib/uploaded_file.rb
@@ -0,0 +1,37 @@
+require "tempfile"
+require "fileutils"
+
+# Taken from: Rack::Test::UploadedFile
+class UploadedFile
+
+ # The filename, *not* including the path, of the "uploaded" file
+ attr_reader :original_filename
+
+ # The tempfile
+ attr_reader :tempfile
+
+ # The content type of the "uploaded" file
+ attr_accessor :content_type
+
+ def initialize(path, filename, content_type = "text/plain")
+ raise "#{path} file does not exist" unless ::File.exist?(path)
+
+ @content_type = content_type
+ @original_filename = filename || ::File.basename(path)
+ @tempfile = File.new(path, 'rb')
+ end
+
+ def path
+ @tempfile.path
+ end
+
+ alias_method :local_path, :path
+
+ def method_missing(method_name, *args, &block) #:nodoc:
+ @tempfile.__send__(method_name, *args, &block)
+ end
+
+ def respond_to?(method_name, include_private = false) #:nodoc:
+ @tempfile.respond_to?(method_name, include_private) || super
+ end
+end
diff --git a/lib/version_check.rb b/lib/version_check.rb
index ea23344948c..91ad07feee5 100644
--- a/lib/version_check.rb
+++ b/lib/version_check.rb
@@ -13,6 +13,6 @@ class VersionCheck
end
def host
- 'https://version.gitlab.com/check.png'
+ 'https://version.gitlab.com/check.svg'
end
end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index dca5e1c5db3..119cc90fc1e 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -1,7 +1,7 @@
#!/bin/bash
if [ -f /.dockerinit ]; then
- wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb
- dpkg -i phantomjs_1.9.0-1+b1_amd64.deb
+ wget -q https://gitlab.com/axil/phantomjs-debian/raw/master/phantomjs_1.9.8-0jessie_amd64.deb
+ dpkg -i phantomjs_1.9.8-0jessie_amd64.deb
apt-get update -qq
apt-get install -y -qq libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client
diff --git a/shared/.gitkeep b/shared/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/.gitkeep
diff --git a/shared/artifacts/.gitkeep b/shared/artifacts/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/artifacts/.gitkeep
diff --git a/shared/artifacts/tmp/cache/.gitkeep b/shared/artifacts/tmp/cache/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/artifacts/tmp/cache/.gitkeep
diff --git a/shared/artifacts/tmp/uploads/.gitkeep b/shared/artifacts/tmp/uploads/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/artifacts/tmp/uploads/.gitkeep
diff --git a/shared/lfs-objects/.gitkeep b/shared/lfs-objects/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/shared/lfs-objects/.gitkeep
diff --git a/spec/benchmarks/finders/issues_finder_spec.rb b/spec/benchmarks/finders/issues_finder_spec.rb
new file mode 100644
index 00000000000..b57a33004a4
--- /dev/null
+++ b/spec/benchmarks/finders/issues_finder_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe IssuesFinder, benchmark: true do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :public) }
+
+ let(:label1) { create(:label, project: project, title: 'A') }
+ let(:label2) { create(:label, project: project, title: 'B') }
+
+ before do
+ 10.times do |n|
+ issue = create(:issue, author: user, project: project)
+
+ if n > 4
+ create(:label_link, label: label1, target: issue)
+ create(:label_link, label: label2, target: issue)
+ end
+ end
+ end
+
+ describe 'retrieving issues without labels' do
+ let(:finder) do
+ IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
+ state: 'opened')
+ end
+
+ benchmark_subject { finder.execute }
+
+ it { is_expected.to iterate_per_second(2000) }
+ end
+
+ describe 'retrieving issues with labels' do
+ let(:finder) do
+ IssuesFinder.new(user, scope: 'all', label_name: label1.title,
+ state: 'opened')
+ end
+
+ benchmark_subject { finder.execute }
+
+ it { is_expected.to iterate_per_second(1000) }
+ end
+
+ describe 'retrieving issues for a single project' do
+ let(:finder) do
+ IssuesFinder.new(user, scope: 'all', label_name: Label::None.title,
+ state: 'opened', project_id: project.id)
+ end
+
+ benchmark_subject { finder.execute }
+
+ it { is_expected.to iterate_per_second(2000) }
+ end
+ end
+end
diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
index 34cd9f7e4eb..3855763b200 100644
--- a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
+++ b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Markdown::ReferenceFilter, benchmark: true do
+describe Banzai::Filter::ReferenceFilter, benchmark: true do
let(:input) do
html = <<-EOF
<p>Hello @alice and @bob, how are you doing today?</p>
diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb
index cc5c3904193..1be7a8d3ed9 100644
--- a/spec/benchmarks/models/user_spec.rb
+++ b/spec/benchmarks/models/user_spec.rb
@@ -1,6 +1,16 @@
require 'spec_helper'
describe User, benchmark: true do
+ describe '.all' do
+ before do
+ 10.times { create(:user) }
+ end
+
+ benchmark_subject { User.all.to_a }
+
+ it { is_expected.to iterate_per_second(500) }
+ end
+
describe '.by_login' do
before do
%w{Alice Bob Eve}.each do |name|
@@ -39,4 +49,30 @@ describe User, benchmark: true do
it { is_expected.to iterate_per_second(iterations) }
end
end
+
+ describe '.find_by_any_email' do
+ let(:user) { create(:user) }
+
+ describe 'using a user with only a single Email address' do
+ let(:email) { user.email }
+
+ benchmark_subject { User.find_by_any_email(email) }
+
+ it { is_expected.to iterate_per_second(1000) }
+ end
+
+ describe 'using a user with multiple Email addresses' do
+ let(:email) { user.emails.first.email }
+
+ benchmark_subject { User.find_by_any_email(email) }
+
+ before do
+ 10.times do
+ user.emails.create(email: FFaker::Internet.email)
+ end
+ end
+
+ it { is_expected.to iterate_per_second(1000) }
+ end
+ end
end
diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb
new file mode 100644
index 00000000000..25ed48c34fd
--- /dev/null
+++ b/spec/benchmarks/services/projects/create_service_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Projects::CreateService, benchmark: true do
+ describe '#execute' do
+ let(:user) { create(:user, :admin) }
+
+ let(:group) do
+ group = create(:group)
+
+ create(:group_member, group: group, user: user)
+
+ group
+ end
+
+ benchmark_subject do
+ name = SecureRandom.hex
+ service = described_class.new(user,
+ name: name,
+ path: name,
+ namespace_id: group.id,
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+
+ service.execute
+ end
+
+ it { is_expected.to iterate_per_second(0.5) }
+ end
+end
diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index 0faab8d7ff0..80a418feb3e 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -1,72 +1,46 @@
require 'spec_helper'
describe AbuseReportsController do
- let(:reporter) { create(:user) }
- let(:user) { create(:user) }
- let(:message) { "This user is a spammer" }
+ let(:reporter) { create(:user) }
+ let(:user) { create(:user) }
+ let(:attrs) do
+ attributes_for(:abuse_report) do |hash|
+ hash[:user_id] = user.id
+ end
+ end
before do
sign_in(reporter)
end
- describe "POST create" do
- context "with admin notification email set" do
- let(:admin_email) { "admin@example.com"}
-
- before(:each) do
- stub_application_setting(admin_notification_email: admin_email)
+ describe 'POST create' do
+ context 'with valid attributes' do
+ it 'saves the abuse report' do
+ expect do
+ post :create, abuse_report: attrs
+ end.to change { AbuseReport.count }.by(1)
end
- it "sends a notification email" do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
-
- email = ActionMailer::Base.deliveries.last
+ it 'calls notify' do
+ expect_any_instance_of(AbuseReport).to receive(:notify)
- expect(email.to).to eq([admin_email])
- expect(email.subject).to include(user.username)
- expect(email.text_part.body).to include(message)
+ post :create, abuse_report: attrs
end
- it "saves the abuse report" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
- end
- end
+ it 'redirects back to the reported user' do
+ post :create, abuse_report: attrs
- context "without admin notification email set" do
- before(:each) do
- stub_application_setting(admin_notification_email: nil)
+ expect(response).to redirect_to user
end
+ end
- it "does not send a notification email" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.not_to change { ActionMailer::Base.deliveries.count }
- end
+ context 'with invalid attributes' do
+ it 'renders new' do
+ attrs.delete(:user_id)
+ post :create, abuse_report: attrs
- it "saves the abuse report" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
+ expect(response).to render_template(:new)
end
end
end
-
end
diff --git a/spec/controllers/admin/impersonation_controller_spec.rb b/spec/controllers/admin/impersonation_controller_spec.rb
new file mode 100644
index 00000000000..d7a7ba1c5b6
--- /dev/null
+++ b/spec/controllers/admin/impersonation_controller_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Admin::ImpersonationController do
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ describe 'CREATE #impersonation when blocked' do
+ let(:blocked_user) { create(:user, state: :blocked) }
+
+ it 'does not allow impersonation' do
+ post :create, id: blocked_user.username
+
+ expect(flash[:alert]).to eq 'You cannot impersonate a blocked user'
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index fcbe62cace8..8b7af4d3a0a 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -7,21 +7,6 @@ describe Admin::UsersController do
sign_in(admin)
end
- describe 'POST login_as' do
- let(:user) { create(:user) }
-
- it 'logs admin as another user' do
- expect(warden.authenticate(scope: :user)).not_to eq(user)
- post :login_as, id: user.username
- expect(warden.authenticate(scope: :user)).to eq(user)
- end
-
- it 'redirects user to homepage' do
- post :login_as, id: user.username
- expect(response).to redirect_to(root_path)
- end
- end
-
describe 'DELETE #user with projects' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index aa8d6cb807f..85379a8e984 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -114,7 +114,7 @@ describe AutocompleteController do
get(:users, project_id: project.id)
end
- it { expect(response.status).to eq(302) }
+ it { expect(response.status).to eq(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(302) }
+ it { expect(response.status).to eq(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(302) }
+ it { expect(response.status).to eq(404) }
end
describe 'GET #users with no project' do
@@ -139,7 +139,8 @@ describe AutocompleteController do
get(:users)
end
- it { expect(response.status).to eq(302) }
+ it { expect(body).to be_kind_of(Array) }
+ it { expect(body.size).to eq 0 }
end
end
end
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index bb3d87f3840..7793bf1e421 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -69,6 +69,21 @@ describe Projects::CommitController do
expect(response.body).to start_with("diff --git")
end
+
+ it "should really only be a git diff without whitespace changes" do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: '66eceea0db202bb39c4e445e8ca28689645366c5',
+ # id: commit.id,
+ format: format,
+ w: 1)
+
+ expect(response.body).to start_with("diff --git")
+ # without whitespace option, there are more than 2 diff_splits
+ diff_splits = assigns(:diffs)[0].diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
end
describe "as patch" do
@@ -95,6 +110,26 @@ describe Projects::CommitController do
expect(response.body).to match(/^diff --git/)
end
end
+
+ context 'commit that removes a submodule' do
+ render_views
+
+ let(:fork_project) { create(:forked_project_with_submodules) }
+ let(:commit) { fork_project.commit('remove-submodule') }
+
+ before do
+ fork_project.team << [user, :master]
+ end
+
+ it 'renders it' do
+ get(:show,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ id: commit.id)
+
+ expect(response).to be_success
+ end
+ end
end
describe "#branches" do
diff --git a/spec/controllers/groups/milestones_controller_spec.rb b/spec/controllers/groups/milestones_controller_spec.rb
new file mode 100644
index 00000000000..eb0c6ac6d80
--- /dev/null
+++ b/spec/controllers/groups/milestones_controller_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Groups::MilestonesController do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+ let(:project2) { create(:empty_project, group: group) }
+ let(:user) { create(:user) }
+ let(:title) { '肯定不是中文的问题' }
+
+ before do
+ sign_in(user)
+ group.add_owner(user)
+ project.team << [user, :master]
+ controller.instance_variable_set(:@group, group)
+ end
+
+ describe "#create" do
+ it "should create group milestone with Chinese title" do
+ post :create,
+ group_id: group.id,
+ milestone: { project_ids: [project.id, project2.id], title: title }
+
+ expect(response).to redirect_to(group_milestone_path(group, title.to_slug.to_s, title: title))
+ expect(Milestone.where(title: title).count).to eq(2)
+ end
+ end
+end
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index 2a447248b70..be19f1abc53 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -23,6 +23,22 @@ describe Projects::CompareController do
expect(assigns(:commits).length).to be >= 1
end
+ it 'compare should show some diffs with ignore whitespace change option' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: '08f22f25',
+ to: '66eceea0',
+ w: 1)
+
+ expect(response).to be_success
+ expect(assigns(:diffs).length).to be >= 1
+ expect(assigns(:commits).length).to be >= 1
+ # without whitespace option, there are more than 2 diff_splits
+ diff_splits = assigns(:diffs)[0].diff.split("\n")
+ expect(diff_splits.length).to be <= 2
+ end
+
describe 'non-existent refs' do
it 'invalid source ref' do
get(:show,
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index b8db8591709..6aaec224f6e 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -10,6 +10,30 @@ describe Projects::MergeRequestsController do
project.team << [user, :master]
end
+ describe '#new' do
+ context 'merge request that removes a submodule' do
+ render_views
+
+ let(:fork_project) { create(:forked_project_with_submodules) }
+
+ before do
+ fork_project.team << [user, :master]
+ end
+
+ it 'renders it' do
+ get :new,
+ namespace_id: fork_project.namespace.to_param,
+ project_id: fork_project.to_param,
+ merge_request: {
+ source_branch: 'remove-submodule',
+ target_branch: 'master'
+ }
+
+ expect(response).to be_success
+ end
+ end
+ end
+
describe "#show" do
shared_examples "export merge as" do |format|
it "should generally work" do
@@ -147,6 +171,34 @@ describe Projects::MergeRequestsController do
end
end
+ describe 'GET diffs with ignore_whitespace_change' do
+ def go(format: 'html')
+ get :diffs,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: merge_request.iid,
+ format: format,
+ w: 1
+ end
+
+ context 'as html' do
+ it 'renders the diff template' do
+ go
+
+ expect(response).to render_template('diffs')
+ end
+ end
+
+ context 'as json' do
+ it 'renders the diffs template to a string' do
+ go format: 'json'
+
+ expect(response).to render_template('projects/merge_requests/show/_diffs')
+ expect(JSON.parse(response.body)).to have_key('html')
+ end
+ end
+ end
+
describe 'GET commits' do
def go(format: 'html')
get :commits,
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 8127efabe6e..d173bb350f1 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -5,7 +5,7 @@ describe Projects::MilestonesController do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
- let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do
sign_in(user)
@@ -15,10 +15,9 @@ describe Projects::MilestonesController do
describe "#destroy" do
it "should remove milestone" do
- merge_request.reload
expect(issue.milestone_id).to eq(milestone.id)
- delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js
+ delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
expect(response).to be_success
expect(Event.first.action).to eq(Event::DESTROYED)
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
index c114f342021..1caa476d37d 100644
--- a/spec/controllers/projects/raw_controller_spec.rb
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -33,5 +33,39 @@ describe Projects::RawController do
expect(response.header['Content-Type']).to eq('image/jpeg')
end
end
+
+ context 'lfs object' do
+ let(:id) { 'be93687/files/lfs/lfs_object.iso' }
+ let!(:lfs_object) { create(:lfs_object, oid: '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', size: '1575078') }
+
+ context 'when project has access' 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 }
+ end
+
+ it 'serves the file' do
+ expect(controller).to receive(:send_file).with("#{Gitlab.config.shared.path}/lfs-objects/91/ef/f75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", filename: "lfs_object.iso", disposition: 'attachment')
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project.to_param,
+ id: id)
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when project does not have access' do
+ it 'does not serve the file' do
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project.to_param,
+ id: id)
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
end
end
diff --git a/spec/controllers/projects/tree_controller_spec.rb b/spec/controllers/projects/tree_controller_spec.rb
index a474574c6e5..e74731c9ed8 100644
--- a/spec/controllers/projects/tree_controller_spec.rb
+++ b/spec/controllers/projects/tree_controller_spec.rb
@@ -98,7 +98,7 @@ describe Projects::TreeController do
project_id: project.to_param,
id: 'master',
dir_name: path,
- new_branch: target_branch,
+ target_branch: target_branch,
commit_message: 'Test commit message')
end
@@ -108,8 +108,8 @@ describe Projects::TreeController do
it 'redirects to the new directory' do
expect(subject).
- to redirect_to("/#{project.path_with_namespace}/blob/#{target_branch}/#{path}")
- expect(flash[:notice]).to eq('The directory has been successfully created')
+ to redirect_to("/#{project.path_with_namespace}/tree/#{target_branch}/#{path}")
+ expect(flash[:notice]).to eq('The directory has been successfully created.')
end
end
@@ -119,7 +119,7 @@ describe Projects::TreeController do
it 'does not allow overwriting of existing files' do
expect(subject).
- to redirect_to("/#{project.path_with_namespace}/blob/master")
+ to redirect_to("/#{project.path_with_namespace}/tree/master")
expect(flash[:alert]).to eq('Directory already exists as a file')
end
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 4460bf12f96..665526fde93 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -51,20 +51,59 @@ describe ProjectsController do
end
context "when requested with case sensitive namespace and project path" do
- it "redirects to the normalized path for case mismatch" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+ context "when there is a match with the same casing" do
+ it "loads the project" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
- expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ expect(assigns(:project)).to eq(public_project)
+ expect(response.status).to eq(200)
+ end
end
- it "loads the page if normalized path matches request path" do
- get :show, namespace_id: public_project.namespace.path, id: public_project.path
+ context "when there is a match with different casing" do
+ it "redirects to the normalized path" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+
+ expect(assigns(:project)).to eq(public_project)
+ 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
- expect(response.status).to eq(200)
+ 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)
+ end
+ end
+ end
end
end
end
+ describe "#destroy" do
+ let(:admin) { create(:admin) }
+
+ it "redirects to the dashboard" do
+ controller.instance_variable_set(:@project, project)
+ sign_in(admin)
+
+ orig_id = project.id
+ 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 redirect_to(dashboard_projects_path)
+ end
+ end
+
describe "POST #toggle_star" do
it "toggles star if user is signed in" do
sign_in(user)
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
new file mode 100644
index 00000000000..b3dcb52c500
--- /dev/null
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -0,0 +1,233 @@
+require 'spec_helper'
+
+describe SnippetsController do
+ describe 'GET #show' do
+ let(:user) { create(:user) }
+
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+
+ it 'responds with status 404' do
+ get :show, id: other_personal_snippet.to_param
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when signed in user is the author' do
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :show, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :show, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'renders the snippet' do
+ get :show, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'responds with status 404' do
+ get :show, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get :show, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+
+ describe 'GET #raw' do
+ let(:user) { create(:user) }
+
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+
+ it 'responds with status 404' do
+ get :raw, id: other_personal_snippet.to_param
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when signed in user is the author' do
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'renders the raw snippet' do
+ get :raw, id: personal_snippet.to_param
+
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
+
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 9f89101d7f7..104a5f50143 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -16,13 +16,26 @@ describe UsersController do
context 'with rendered views' do
render_views
- it 'renders the show template' do
- sign_in(user)
+ describe 'when logged in' do
+ before do
+ sign_in(user)
+ end
- get :show, username: user.username
+ it 'renders the show template' do
+ get :show, username: user.username
- expect(response).to be_success
- expect(response).to render_template('show')
+ expect(response).to be_success
+ expect(response).to render_template('show')
+ end
+ end
+
+ describe 'when logged out' do
+ it 'renders the show template' do
+ get :show, username: user.username
+
+ expect(response).to be_success
+ expect(response).to render_template('show')
+ end
end
end
end
diff --git a/spec/factories.rb b/spec/factories.rb
index 200f18f660d..d6b4efa9a03 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -43,7 +43,8 @@ FactoryGirl.define do
end
after(:create) do |user, evaluator|
- user.identities << create(:identity,
+ user.identities << create(
+ :identity,
provider: evaluator.provider,
extern_uid: evaluator.extern_uid
)
@@ -165,6 +166,18 @@ FactoryGirl.define do
title
content
file_name
+
+ trait :public do
+ visibility_level Gitlab::VisibilityLevel::PUBLIC
+ end
+
+ trait :internal do
+ visibility_level Gitlab::VisibilityLevel::INTERNAL
+ end
+
+ trait :private do
+ visibility_level Gitlab::VisibilityLevel::PRIVATE
+ end
end
factory :snippet do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index 2fcd70182b9..f76e826f138 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -42,6 +42,10 @@ FactoryGirl.define do
commit factory: :ci_commit
+ after(:build) do |build, evaluator|
+ build.project = build.commit.project
+ end
+
factory :ci_not_started_build do
started_at nil
finished_at nil
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb
index 79e000b7ccb..b42cafa518a 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/commits.rb
@@ -2,17 +2,18 @@
#
# Table name: commits
#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+# gl_project_id :integer
#
# Read about factories at https://github.com/thoughtbot/factory_girl
@@ -20,7 +21,7 @@ FactoryGirl.define do
factory :ci_empty_commit, class: Ci::Commit do
sha '97de212e80737a608d939f648d959671fb0a0142'
- gl_project factory: :empty_project
+ project factory: :empty_project
factory :ci_commit_without_jobs do
after(:build) do |commit|
diff --git a/spec/factories/ci/events.rb b/spec/factories/ci/events.rb
deleted file mode 100644
index 9638618a400..00000000000
--- a/spec/factories/ci/events.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# == Schema Information
-#
-# Table name: events
-#
-# id :integer not null, primary key
-# project_id :integer
-# user_id :integer
-# is_admin :integer
-# description :text
-# created_at :datetime
-# updated_at :datetime
-#
-
-FactoryGirl.define do
- factory :ci_event, class: Ci::Event do
- sequence :description do |n|
- "updated project settings#{n}"
- end
-
- factory :ci_admin_event do
- is_admin true
- end
- end
-end
diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb
deleted file mode 100644
index 111e1a82816..00000000000
--- a/spec/factories/ci/projects.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# == Schema Information
-#
-# Table name: projects
-#
-# id :integer not null, primary key
-# name :string(255) not null
-# timeout :integer default(3600), not null
-# created_at :datetime
-# updated_at :datetime
-# token :string(255)
-# default_ref :string(255)
-# path :string(255)
-# always_build :boolean default(FALSE), not null
-# polling_interval :integer
-# public :boolean default(FALSE), not null
-# ssh_url_to_repo :string(255)
-# gitlab_id :integer
-# allow_git_fetch :boolean default(TRUE), not null
-# email_recipients :string(255) default(""), not null
-# email_add_pusher :boolean default(TRUE), not null
-# email_only_broken_builds :boolean default(TRUE), not null
-# skip_refs :string(255)
-# coverage_regex :string(255)
-# shared_runners_enabled :boolean default(FALSE)
-# generated_yaml_config :text
-#
-
-# Read about factories at https://github.com/thoughtbot/factory_girl
-
-FactoryGirl.define do
- factory :ci_project_without_token, class: Ci::Project do
- default_ref 'master'
-
- gl_project factory: :empty_project
-
- factory :ci_project do
- token 'iPWx6WM4lhHNedGfBpPJNP'
- end
-
- factory :ci_public_project do
- public true
- end
- end
-end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 3aa14ca434d..008d1c5d961 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -14,6 +14,6 @@
FactoryGirl.define do
factory :ci_runner_project, class: Ci::RunnerProject do
runner_id 1
- project_id 1
+ gl_project_id 1
end
end
diff --git a/spec/factories/ci/web_hook.rb b/spec/factories/ci/web_hook.rb
deleted file mode 100644
index 40d878ecb3c..00000000000
--- a/spec/factories/ci/web_hook.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-FactoryGirl.define do
- factory :ci_web_hook, class: Ci::WebHook do
- sequence(:url) { FFaker::Internet.uri('http') }
- project factory: :ci_project
- end
-end
diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb
index 52de437052d..8898b71e2a3 100644
--- a/spec/factories/commit_statuses.rb
+++ b/spec/factories/commit_statuses.rb
@@ -5,7 +5,7 @@ FactoryGirl.define do
name 'default'
status 'success'
description 'commit status'
- commit factory: :ci_commit
+ commit factory: :ci_commit_with_one_job
factory :generic_commit_status, class: GenericCommitStatus do
name 'generic'
diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb
index 6829387c660..8b12ee11af5 100644
--- a/spec/factories/labels.rb
+++ b/spec/factories/labels.rb
@@ -8,6 +8,7 @@
# project_id :integer
# created_at :datetime
# updated_at :datetime
+# template :boolean default(FALSE)
#
# Read about factories at https://github.com/thoughtbot/factory_girl
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
new file mode 100644
index 00000000000..2da107ba24b
--- /dev/null
+++ b/spec/factories/lfs_objects.rb
@@ -0,0 +1,24 @@
+# == Schema Information
+#
+# Table name: lfs_objects
+#
+# id :integer not null, primary key
+# oid :string(255) not null
+# size :integer not null
+# created_at :datetime
+# updated_at :datetime
+# file :string(255)
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :lfs_object do
+ oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80"
+ size 499013
+ end
+
+ trait :with_file do
+ file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
+ end
+end
diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb
new file mode 100644
index 00000000000..3772236a77a
--- /dev/null
+++ b/spec/factories/lfs_objects_projects.rb
@@ -0,0 +1,19 @@
+# == Schema Information
+#
+# Table name: lfs_objects_projects
+#
+# id :integer not null, primary key
+# lfs_object_id :integer not null
+# project_id :integer not null
+# created_at :datetime
+# updated_at :datetime
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :lfs_objects_project do
+ lfs_object
+ project
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 6080d0ccdef..5b4d7f41bc4 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -20,6 +20,7 @@
# position :integer default(0)
# locked_at :datetime
# updated_by_id :integer
+# merge_error :string(255)
#
FactoryGirl.define do
@@ -64,6 +65,11 @@ FactoryGirl.define do
target_branch "master"
end
+ trait :merge_when_build_succeeds do
+ merge_when_build_succeeds true
+ merge_user author
+ end
+
factory :closed_merge_request, traits: [:closed]
factory :reopened_merge_request, traits: [:reopened]
factory :merge_request_with_diffs, traits: [:with_diffs]
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 9d777ddfccd..35a20adeef3 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -16,6 +16,7 @@
# system :boolean default(FALSE), not null
# st_diff :text
# updated_by_id :integer
+# is_award :boolean default(FALSE), not null
#
require_relative '../support/repo_helpers'
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1d500a11ad7..112213377ff 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -28,6 +28,7 @@
# import_type :string(255)
# import_source :string(255)
# commit_count :integer default(0)
+# import_error :text
#
FactoryGirl.define do
diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb
new file mode 100644
index 00000000000..43d09b17534
--- /dev/null
+++ b/spec/factories/releases.rb
@@ -0,0 +1,21 @@
+# == Schema Information
+#
+# Table name: releases
+#
+# id :integer not null, primary key
+# tag :string(255)
+# description :text
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :release do
+ tag "v1.1.0"
+ description "Awesome release"
+ project
+ end
+end
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
new file mode 100644
index 00000000000..b955d0b0c46
--- /dev/null
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+describe 'Admin Builds' do
+ before do
+ login_as :admin
+ end
+
+ describe 'GET /admin/builds' do
+ let(:commit) { create(:ci_commit) }
+
+ context 'All tab' do
+ context 'when have builds' do
+ it 'shows all builds' do
+ create(:ci_build, commit: commit, status: :pending)
+ create(:ci_build, commit: commit, status: :running)
+ create(:ci_build, commit: commit, status: :success)
+ create(:ci_build, commit: commit, status: :failed)
+
+ visit admin_builds_path
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'All')
+ expect(page.all('.build-link').size).to eq(4)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
+
+ context 'when have no builds' do
+ it 'shows a message' do
+ visit admin_builds_path
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'All')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
+ end
+
+ context 'Running tab' do
+ context 'when have running builds' do
+ it 'shows running builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :success)
+ build3 = create(:ci_build, commit: commit, status: :failed)
+
+ visit admin_builds_path(scope: :running)
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running')
+ expect(page.find('.build-link')).to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).not_to have_content(build3.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
+
+ context 'when have no builds running' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :success)
+
+ visit admin_builds_path(scope: :running)
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running')
+ expect(page).to have_content 'No builds to show'
+ expect(page).not_to have_link 'Cancel all'
+ end
+ end
+ end
+
+ context 'Finished tab' do
+ context 'when have finished builds' do
+ it 'shows finished builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :running)
+ build3 = create(:ci_build, commit: commit, status: :success)
+
+ visit admin_builds_path(scope: :finished)
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished')
+ expect(page.find('.build-link')).not_to have_content(build1.id)
+ expect(page.find('.build-link')).not_to have_content(build2.id)
+ expect(page.find('.build-link')).to have_content(build3.id)
+ expect(page).to have_link 'Cancel all'
+ end
+ end
+
+ context 'when have no builds finished' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :running)
+
+ visit admin_builds_path(scope: :finished)
+
+ expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished')
+ expect(page).to have_content 'No builds to show'
+ expect(page).to have_link 'Cancel all'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index b83744f53a8..26d03944b8a 100644
--- a/spec/features/ci/admin/runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -10,7 +10,7 @@ describe "Admin Runners" do
runner = FactoryGirl.create(:ci_runner)
commit = FactoryGirl.create(:ci_commit)
FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id)
- visit ci_admin_runners_path
+ visit admin_runners_path
end
it { page.has_text? "Manage Runners" }
@@ -36,9 +36,9 @@ describe "Admin Runners" do
let(:runner) { FactoryGirl.create :ci_runner }
before do
- @project1 = FactoryGirl.create(:ci_project)
- @project2 = FactoryGirl.create(:ci_project)
- visit ci_admin_runner_path(runner)
+ @project1 = FactoryGirl.create(:empty_project)
+ @project2 = FactoryGirl.create(:empty_project)
+ visit admin_runner_path(runner)
end
describe 'runner info' do
@@ -53,7 +53,7 @@ describe "Admin Runners" do
describe 'search' do
before do
search_form = find('#runner-projects-search')
- search_form.fill_in 'search', with: @project1.gl_project.name
+ search_form.fill_in 'search', with: @project1.name
search_form.click_button 'Search'
end
@@ -61,4 +61,26 @@ describe "Admin Runners" do
it { expect(page).not_to have_content(@project2.name_with_namespace) }
end
end
+
+ describe 'runners registration token' do
+ let!(:token) { current_application_settings.runners_registration_token }
+ before { visit admin_runners_path }
+
+ it 'has a registration token' do
+ expect(page).to have_content("Registration token is #{token}")
+ expect(page).to have_selector('#runners-token', text: token)
+ end
+
+ describe 'reload registration token' do
+ let(:page_token) { find('#runners-token').text }
+
+ before do
+ click_button 'Reset runners registration token'
+ end
+
+ it 'changes registration token' do
+ expect(page_token).to_not eq token
+ end
+ end
+ end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index c2c7364f6c5..4570e409128 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -87,13 +87,16 @@ describe "Admin::Users", feature: true do
end
it "should call send mail" do
- expect(Notify).to receive(:new_user_email)
+ expect_any_instance_of(NotificationService).to receive(:new_user)
click_button "Create user"
end
it "should send valid email to user with email & password" do
- click_button "Create user"
+ perform_enqueued_jobs do
+ click_button "Create user"
+ end
+
user = User.find_by(username: 'bang')
email = ActionMailer::Base.deliveries.last
expect(email.subject).to have_content('Account was created')
@@ -111,24 +114,60 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(@user.name)
end
- describe 'Login as another user' do
- it 'should show login button for other users and check that it works' do
- another_user = create(:user)
+ describe 'Impersonation' do
+ let(:another_user) { create(:user) }
+ before { visit admin_user_path(another_user) }
+
+ context 'before impersonating' do
+ it 'shows impersonate button for other users' do
+ expect(page).to have_content('Impersonate')
+ end
+
+ it 'should not show impersonate button for admin itself' do
+ visit admin_user_path(@user)
+
+ expect(page).not_to have_content('Impersonate')
+ end
- visit admin_user_path(another_user)
+ it 'should not show impersonate button for blocked user' do
+ another_user.block
- click_link 'Log in as this user'
+ visit admin_user_path(another_user)
- expect(page).to have_content("Logged in as #{another_user.username}")
+ expect(page).not_to have_content('Impersonate')
- page.within '.sidebar-user .username' do
- expect(page).to have_content(another_user.username)
+ another_user.activate
end
end
- it 'should not show login button for admin itself' do
- visit admin_user_path(@user)
- expect(page).not_to have_content('Log in as this user')
+ context 'when impersonating' 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
+ end
+
+ it 'sees impersonation log out icon' do
+ icon = first('.fa.fa-user-secret')
+
+ expect(icon).to_not 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
+ end
+
+ it 'is redirected back to the impersonated users page in the admin after stopping' do
+ find(:css, 'li.impersonation a').click
+
+ expect(current_path).to eql "/admin/users/#{another_user.username}"
+ end
end
end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 154857e77fe..240e56839df 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -1,23 +1,25 @@
require 'spec_helper'
describe "Builds" do
+ let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+
before do
login_as(:user)
@commit = FactoryGirl.create :ci_commit
@build = FactoryGirl.create :ci_build, commit: @commit
- @gl_project = @commit.project.gl_project
- @gl_project.team << [@user, :master]
+ @project = @commit.project
+ @project.team << [@user, :master]
end
describe "GET /:project/builds" do
context "Running scope" do
before do
@build.run!
- visit namespace_project_builds_path(@gl_project.namespace, @gl_project)
+ visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
end
- it { expect(page).to have_content 'Running' }
- it { expect(page).to have_content 'Cancel all' }
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Running') }
+ it { expect(page).to have_link 'Cancel running' }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
@@ -26,51 +28,67 @@ describe "Builds" do
context "Finished scope" do
before do
@build.run!
- visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :finished)
+ visit namespace_project_builds_path(@project.namespace, @project, scope: :finished)
end
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'Finished') }
it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to have_content 'Cancel all' }
+ it { expect(page).to have_link 'Cancel running' }
end
context "All builds" do
before do
- @gl_project.ci_builds.running_or_pending.each(&:success)
- visit namespace_project_builds_path(@gl_project.namespace, @gl_project, scope: :all)
+ @project.builds.running_or_pending.each(&:success)
+ visit namespace_project_builds_path(@project.namespace, @project)
end
- it { expect(page).to have_content 'All' }
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') }
it { expect(page).to have_content @build.short_sha }
it { expect(page).to have_content @build.ref }
it { expect(page).to have_content @build.name }
- it { expect(page).to_not have_content 'Cancel all' }
+ it { expect(page).to_not have_link 'Cancel running' }
end
end
- describe "GET /:project/builds/:id/cancel_all" do
+ describe "POST /:project/builds/:id/cancel_all" do
before do
@build.run!
- visit cancel_all_namespace_project_builds_path(@gl_project.namespace, @gl_project)
+ visit namespace_project_builds_path(@project.namespace, @project)
+ click_link "Cancel running"
end
- it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to_not have_content 'Cancel all' }
+ it { expect(page).to have_selector('.project-issuable-filter li.active', text: 'All') }
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content @build.short_sha }
+ it { expect(page).to have_content @build.ref }
+ it { expect(page).to have_content @build.name }
+ it { expect(page).to_not have_link 'Cancel running' }
end
describe "GET /:project/builds/:id" do
before do
- visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
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 "Download artifacts" do
+ before do
+ @build.update_attributes(artifacts_file: artifacts_file)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it { expect(page).to have_content 'Download artifacts' }
+ end
end
- describe "GET /:project/builds/:id/cancel" do
+ describe "POST /:project/builds/:id/cancel" do
before do
@build.run!
- visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link "Cancel"
end
it { expect(page).to have_content 'canceled' }
@@ -79,11 +97,23 @@ describe "Builds" do
describe "POST /:project/builds/:id/retry" do
before do
- visit cancel_namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ click_link "Cancel"
click_link 'Retry'
end
it { expect(page).to have_content 'pending' }
it { expect(page).to have_content 'Cancel' }
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)
+ click_link 'Download artifacts'
+ end
+
+ it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
+ end
end
diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb
deleted file mode 100644
index 623d466c67b..00000000000
--- a/spec/features/ci/admin/builds_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require 'spec_helper'
-
-describe "Admin Builds" do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
-
- before do
- skip_ci_admin_auth
- login_as :user
- end
-
- describe "GET /admin/builds" do
- before do
- build
- visit ci_admin_builds_path
- end
-
- it { expect(page).to have_content "All builds" }
- it { expect(page).to have_content build.short_sha }
- end
-
- describe "Tabs" do
- it "shows all builds" do
- FactoryGirl.create :ci_build, commit: commit, status: "pending"
- FactoryGirl.create :ci_build, commit: commit, status: "running"
- FactoryGirl.create :ci_build, commit: commit, status: "success"
- FactoryGirl.create :ci_build, commit: commit, status: "failed"
-
- visit ci_admin_builds_path
-
- expect(page.all(".build-link").size).to eq(4)
- end
-
- it "shows pending builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
- build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
-
- visit ci_admin_builds_path
-
- within ".nav.nav-tabs" do
- click_on "Pending"
- end
-
- expect(page.find(".build-link")).to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build1.id)
- expect(page.find(".build-link")).not_to have_content(build2.id)
- expect(page.find(".build-link")).not_to have_content(build3.id)
- end
-
- it "shows running builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
- build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
-
- visit ci_admin_builds_path
-
- within ".nav.nav-tabs" do
- click_on "Running"
- end
-
- expect(page.find(".build-link")).to have_content(build1.id)
- expect(page.find(".build-link")).not_to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build2.id)
- expect(page.find(".build-link")).not_to have_content(build3.id)
- end
- end
-end
diff --git a/spec/features/ci/admin/events_spec.rb b/spec/features/ci/admin/events_spec.rb
deleted file mode 100644
index a7e75cc4f6b..00000000000
--- a/spec/features/ci/admin/events_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe "Admin Events" do
- let(:event) { FactoryGirl.create :ci_admin_event }
-
- before do
- skip_ci_admin_auth
- login_as :user
- end
-
- describe "GET /admin/events" do
- before do
- event
- visit ci_admin_events_path
- end
-
- it { expect(page).to have_content "Events" }
- it { expect(page).to have_content event.description }
- end
-end
diff --git a/spec/features/ci/admin/projects_spec.rb b/spec/features/ci/admin/projects_spec.rb
deleted file mode 100644
index b88f55a6807..00000000000
--- a/spec/features/ci/admin/projects_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require 'spec_helper'
-
-describe "Admin Projects" do
- let(:project) { FactoryGirl.create :ci_project }
-
- before do
- skip_ci_admin_auth
- login_as :user
- end
-
- describe "GET /admin/projects" do
- before do
- project
- visit ci_admin_projects_path
- end
-
- it { expect(page).to have_content "Projects" }
- end
-end
diff --git a/spec/features/ci/events_spec.rb b/spec/features/ci/events_spec.rb
deleted file mode 100644
index 5b9fd404159..00000000000
--- a/spec/features/ci/events_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'spec_helper'
-
-describe "Events" do
- let(:user) { create(:user) }
- let(:project) { FactoryGirl.create :ci_project }
- let(:event) { FactoryGirl.create :ci_admin_event, project: project }
-
- before do
- login_as(user)
- project.gl_project.team << [user, :master]
- end
-
- describe "GET /ci/project/:id/events" do
- before do
- event
- visit ci_project_events_path(project)
- end
-
- it { expect(page).to have_content "Events" }
- it { expect(page).to have_content event.description }
- end
-end
diff --git a/spec/features/ci/lint_spec.rb b/spec/features/ci/lint_spec.rb
deleted file mode 100644
index 5d8f56e2cfb..00000000000
--- a/spec/features/ci/lint_spec.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'spec_helper'
-
-describe "Lint" do
- before do
- login_as :user
- end
-
- it "Yaml parsing", js: true do
- content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
- visit ci_lint_path
- fill_in "content", with: content
- click_on "Validate"
- within "table" do
- expect(page).to have_content("Job - rspec")
- expect(page).to have_content("Job - spinach")
- expect(page).to have_content("Deploy Job - staging")
- expect(page).to have_content("Deploy Job - production")
- end
- end
-
- it "Yaml parsing with error", js: true do
- visit ci_lint_path
- fill_in "content", with: ""
- click_on "Validate"
- expect(page).to have_content("Status: syntax is incorrect")
- expect(page).to have_content("Error: Please provide content of .gitlab-ci.yml")
- end
-end
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
new file mode 100644
index 00000000000..e6e73e5e67c
--- /dev/null
+++ b/spec/features/ci_lint_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe 'CI Lint' do
+ before do
+ login_as :user
+ end
+
+ describe 'YAML parsing' do
+ before do
+ visit ci_lint_path
+ fill_in 'content', with: yaml_content
+ click_on 'Validate'
+ end
+
+ context 'YAML is correct' do
+ let(:yaml_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ it 'Yaml parsing' do
+ within "table" do
+ expect(page).to have_content('Job - rspec')
+ expect(page).to have_content('Job - spinach')
+ expect(page).to have_content('Deploy Job - staging')
+ expect(page).to have_content('Deploy Job - production')
+ end
+ end
+ end
+
+ context 'YAML is incorrect' do
+ let(:yaml_content) { '' }
+
+ it 'displays information about an error' do
+ expect(page).to have_content('Status: syntax is incorrect')
+ expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml')
+ end
+ end
+ end
+end
diff --git a/spec/features/ci_settings_spec.rb b/spec/features/ci_settings_spec.rb
deleted file mode 100644
index 7e25e883018..00000000000
--- a/spec/features/ci_settings_spec.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'spec_helper'
-
-describe "CI settings" do
- let(:user) { create(:user) }
- before { login_as(user) }
-
- before do
- @project = FactoryGirl.create :ci_project
- @gl_project = @project.gl_project
- @gl_project.team << [user, :master]
- visit edit_namespace_project_ci_settings_path(@gl_project.namespace, @gl_project)
- end
-
- it { expect(page).to have_content 'Build Schedule' }
-
- it "updates configuration" do
- fill_in 'Timeout', with: '70'
- click_button 'Save changes'
- expect(page).to have_content 'was successfully updated'
- expect(find_field('Timeout').value).to eq '70'
- end
-end
diff --git a/spec/features/ci_web_hooks_spec.rb b/spec/features/ci_web_hooks_spec.rb
deleted file mode 100644
index efae0a42c1e..00000000000
--- a/spec/features/ci_web_hooks_spec.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'spec_helper'
-
-describe 'CI web hooks' do
- let(:user) { create(:user) }
- before { login_as(user) }
-
- before do
- @project = FactoryGirl.create :ci_project
- @gl_project = @project.gl_project
- @gl_project.team << [user, :master]
- visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project)
- end
-
- context 'create a trigger' do
- before do
- fill_in 'web_hook_url', with: 'http://example.com'
- click_on 'Add Web Hook'
- end
-
- it { expect(@project.web_hooks.count).to eq(1) }
-
- it 'revokes the trigger' do
- click_on 'Remove'
- expect(@project.web_hooks.count).to eq(0)
- end
- end
-end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 1adc2cdf70a..fe7f07f5b75 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -1,60 +1,99 @@
require 'spec_helper'
-describe "Commits" do
+describe 'Commits' do
include CiStatusHelper
let(:project) { create(:project) }
- describe "CI" do
+ describe 'CI' do
before do
login_as :user
project.team << [@user, :master]
- @ci_project = project.ensure_gitlab_ci_project
- @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha
- @build = FactoryGirl.create :ci_build, commit: @commit
- @generic_status = FactoryGirl.create :generic_commit_status, commit: @commit
+ stub_ci_commit_to_return_yaml_file
end
- before do
- stub_ci_commit_to_return_yaml_file
+ let!(:commit) do
+ FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha
end
- describe "GET /:project/commits/:sha" do
+ let!(:build) { FactoryGirl.create :ci_build, commit: commit }
+
+ describe 'Project commits' do
before do
- visit ci_status_path(@commit)
+ visit namespace_project_commits_path(project.namespace, project, :master)
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 'should show build status' do
+ page.within("//li[@id='commit-#{commit.short_sha}']") do
+ expect(page).to have_css(".ci-status-link")
+ end
+ end
end
- describe "Cancel all builds" do
- it "cancels commit" do
- visit ci_status_path(@commit)
- click_on "Cancel all"
- expect(page).to have_content "canceled"
+ describe 'Commit builds' do
+ before do
+ visit ci_status_path(commit)
end
+
+ it { expect(page).to have_content commit.sha[0..7] }
+ it { expect(page).to have_content commit.git_commit_message }
+ it { expect(page).to have_content commit.git_author_name }
end
- describe "Cancel build" do
- it "cancels build" do
- visit ci_status_path(@commit)
- click_on "Cancel"
- expect(page).to have_content "canceled"
+ context 'Download artifacts' do
+ let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+
+ before do
+ build.update_attributes(artifacts_file: artifacts_file)
+ end
+
+ it do
+ visit ci_status_path(commit)
+ click_on 'Download artifacts'
+ expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type)
end
end
- describe ".gitlab-ci.yml not found warning" do
- it "does not show warning" do
- visit ci_status_path(@commit)
- expect(page).not_to have_content ".gitlab-ci.yml not found in this commit"
+ describe 'Cancel all builds' do
+ it 'cancels commit' do
+ visit ci_status_path(commit)
+ click_on 'Cancel running'
+ expect(page).to have_content 'canceled'
end
+ end
+
+ describe 'Cancel build' do
+ it 'cancels build' do
+ visit ci_status_path(commit)
+ click_on 'Cancel'
+ expect(page).to have_content 'canceled'
+ end
+ end
+
+ describe '.gitlab-ci.yml not found warning' do
+ context 'ci builds enabled' do
+ it "does not show warning" do
+ visit ci_status_path(commit)
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
+
+ it 'shows warning' do
+ stub_ci_commit_yaml_file(nil)
+ visit ci_status_path(commit)
+ expect(page).to have_content '.gitlab-ci.yml not found in this commit'
+ end
+ end
+
+ context 'ci builds disabled' do
+ before do
+ stub_ci_builds_disabled
+ stub_ci_commit_yaml_file(nil)
+ visit ci_status_path(commit)
+ end
- it "shows warning" do
- stub_ci_commit_yaml_file(nil)
- visit ci_status_path(@commit)
- expect(page).to have_content ".gitlab-ci.yml not found in this commit"
+ it 'does not show warning' do
+ expect(page).not_to have_content '.gitlab-ci.yml not found in this commit'
+ end
end
end
end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index f600f8684ac..38c8d343ce3 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -13,7 +13,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(Milestone::None.title)
- expect(page).to have_css('.issue-title', count: 1)
+ expect(page).to have_css('.title', count: 1)
end
scenario 'filters by a specific Milestone', js: true do
@@ -23,7 +23,7 @@ feature 'Issue filtering by Milestone', feature: true do
visit_issues(project)
filter_by_milestone(milestone.title)
- expect(page).to have_css('.issue-title', count: 1)
+ expect(page).to have_css('.title', count: 1)
end
def visit_issues(project)
diff --git a/spec/features/issues/note_polling_spec.rb b/spec/features/issues/note_polling_spec.rb
new file mode 100644
index 00000000000..e4efdbe2421
--- /dev/null
+++ b/spec/features/issues/note_polling_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Issue notes polling' do
+ let!(:project) { create(:project, :public) }
+ let!(:issue) { create(:issue, project: project) }
+
+ background do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ scenario 'Another user adds a comment to an issue', js: true do
+ note = create(:note_on_issue, noteable: issue, 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_spec.rb b/spec/features/issues_spec.rb
index 32fd4065bb4..a2fb3e4c75d 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -61,7 +61,7 @@ describe 'Issues', feature: true do
it 'allows user to select unasigned', js: true do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- expect(page).to have_content "Assign to #{@user.name}"
+ expect(page).to have_content "Assignee #{@user.name}"
first('#s2id_issue_assignee_id').click
sleep 2 # wait for ajax stuff to complete
@@ -69,7 +69,10 @@ describe 'Issues', feature: true do
click_button 'Save changes'
- expect(page).to have_content 'Assignee: none'
+ page.within('.assignee') do
+ expect(page).to have_content 'None'
+ end
+
expect(issue.reload.assignee).to be_nil
end
end
@@ -202,11 +205,11 @@ describe 'Issues', feature: true do
it 'with dropdown menu' do
visit namespace_project_issue_path(project.namespace, project, issue)
- find('.context #issue_assignee_id').
+ find('.issuable-sidebar #issue_assignee_id').
set project.team.members.first.id
click_button 'Update Issue'
- expect(page).to have_content 'Assignee:'
+ expect(page).to have_content 'Assignee'
has_select?('issue_assignee_id',
selected: project.team.members.first.name)
end
@@ -241,12 +244,16 @@ describe 'Issues', feature: true do
it 'with dropdown menu' do
visit namespace_project_issue_path(project.namespace, project, issue)
- find('.context').
+ find('.issuable-sidebar').
select(milestone.title, from: 'issue_milestone_id')
click_button 'Update Issue'
expect(page).to have_content "Milestone changed to #{milestone.title}"
- expect(page).to have_content "Milestone: #{milestone.title}"
+
+ page.within('.milestone') do
+ expect(page).to have_content milestone.title
+ end
+
has_select?('issue_assignee_id', selected: milestone.title)
end
end
@@ -279,13 +286,19 @@ describe 'Issues', feature: true do
it 'allows user to remove assignee', js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
- expect(page).to have_content "Assignee: #{user2.name}"
- first('#s2id_issue_assignee_id').click
+ page.within('.assignee') do
+ expect(page).to have_content user2.name
+ end
+
+ find('.assignee .edit-link').click
sleep 2 # wait for ajax stuff to complete
first('.user-result').click
- expect(page).to have_content 'Assignee: none'
+ page.within('.assignee') do
+ expect(page).to have_content 'None'
+ end
+
sleep 2 # wait for ajax stuff to complete
expect(issue.reload.assignee).to be_nil
end
@@ -293,10 +306,10 @@ describe 'Issues', feature: true do
end
def first_issue
- page.all('ul.issues-list li').first.text
+ page.all('ul.issues-list > li').first.text
end
def last_issue
- page.all('ul.issues-list li').last.text
+ page.all('ul.issues-list > li').last.text
end
end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index 922c76285d1..2451e56fe7c 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -98,4 +98,56 @@ feature 'Login', feature: true do
expect(page).to have_content('Invalid login or password.')
end
end
+
+ describe 'with required two-factor authentication enabled' do
+ let(:user) { create(:user) }
+ before(:each) { stub_application_setting(require_two_factor_authentication: true) }
+
+ context 'with grace period defined' do
+ before(:each) do
+ stub_application_setting(two_factor_grace_period: 48)
+ login_with(user)
+ end
+
+ context 'within the grace period' do
+ it 'redirects to two-factor configuration page' do
+ expect(current_path).to eq new_profile_two_factor_auth_path
+ expect(page).to have_content('You must configure Two-Factor Authentication in your account until')
+ end
+
+ it 'two-factor configuration is skippable' do
+ expect(current_path).to eq new_profile_two_factor_auth_path
+
+ click_link 'Configure it later'
+ expect(current_path).to eq root_path
+ end
+ end
+
+ context 'after the grace period' 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 configure Two-Factor Authentication in your account.')
+ end
+
+ it 'two-factor configuration is not skippable' do
+ expect(current_path).to eq new_profile_two_factor_auth_path
+ expect(page).not_to have_link('Configure it later')
+ end
+ end
+ end
+
+ context 'without grace pariod 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 configure Two-Factor Authentication in your account.')
+ end
+ end
+ 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
new file mode 100644
index 00000000000..7aa7eb965e9
--- /dev/null
+++ b/spec/features/merge_requests/merge_when_build_succeeds_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+feature 'Merge When Build Succeeds', feature: true, js: true do
+ let(:user) { create(:user) }
+
+ let(:project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") }
+
+ before do
+ project.team << [user, :master]
+ project.enable_ci
+ 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) }
+
+ before do
+ login_as user
+ visit_merge_request(merge_request)
+ end
+
+ it 'displays the Merge When Build Succeeds button' do
+ expect(page).to have_button "Merge When Build Succeeds"
+ end
+
+ context "Merge When Build succeeds enabled" do
+ before do
+ click_button "Merge When Build Succeeds"
+ end
+
+ it 'activates Merge When Build Succeeds feature' do
+ expect(page).to have_link "Cancel Automatic Merge"
+
+ expect(page).to have_content "Set by #{user.name} to be merged automatically when the build succeeds."
+ expect(page).to have_content "The source branch will not be removed."
+
+ visit_merge_request(merge_request) # Needed to refresh the page
+ expect(page).to have_content /Enabled an automatic merge when the build for [0-9a-f]{8} succeeds/i
+ end
+ end
+ end
+
+ context 'When it is enabled' do
+ let(:merge_request) do
+ create(:merge_request_with_diffs, :simple, source_project: project, author: user,
+ 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) }
+
+ before do
+ login_as user
+ visit_merge_request(merge_request)
+ end
+
+ it 'cancels the automatic merge' do
+ click_link "Cancel Automatic Merge"
+
+ expect(page).to have_button "Merge When Build Succeeds"
+
+ visit_merge_request(merge_request) # Needed to refresh the page
+ expect(page).to have_content "Canceled the automatic merge"
+ end
+
+ it "allows the user to remove the source branch" do
+ expect(page).to have_link "Remove Source Branch When Merged"
+
+ click_link "Remove Source Branch When Merged"
+ expect(page).to have_content "The source branch will be removed"
+ end
+ end
+
+ context 'Build is not active' do
+ it "should not allow for enabling" do
+ visit_merge_request(merge_request)
+ expect(page).not_to have_link "Merge When Build Succeeds"
+ 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/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index d7cb3b2e86e..f0fc6916c4d 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe 'Comments', feature: true do
include RepoHelpers
+ include WaitForAjax
describe 'On a merge request', js: true, feature: true do
let!(:merge_request) { create(:merge_request) }
@@ -123,8 +124,8 @@ describe 'Comments', feature: true do
it 'removes the attachment div and resets the edit form' do
find('.js-note-attachment-delete').click
is_expected.not_to have_css('.note-attachment')
- expect(find('.current-note-edit-form', visible: false)).
- not_to be_visible
+ is_expected.not_to have_css('.current-note-edit-form')
+ wait_for_ajax
end
end
end
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index 85e70b4d47f..257d363438c 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -3,11 +3,12 @@ require 'spec_helper'
feature 'Password reset', feature: true do
describe 'throttling' do
it 'sends reset instructions when not previously sent' do
- visit root_path
- forgot_password(create(:user))
+ user = create(:user)
+ forgot_password(user)
- expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
+ expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions'))
expect(current_path).to eq new_user_session_path
+ expect(user.recently_sent_password_reset?).to be_truthy
end
it 'sends reset instructions when previously sent more than a minute ago' do
@@ -15,26 +16,25 @@ feature 'Password reset', feature: true do
user.send_reset_password_instructions
user.update_attribute(:reset_password_sent_at, 5.minutes.ago)
- visit root_path
- forgot_password(user)
-
- expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
+ expect{ forgot_password(user) }.to change{ user.reset_password_sent_at }
+ expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions'))
expect(current_path).to eq new_user_session_path
end
- it "throttles multiple resets in a short timespan" do
+ it 'throttles multiple resets in a short timespan' do
user = create(:user)
user.send_reset_password_instructions
+ # Reload because PG handles datetime less precisely than Ruby/Rails
+ user.reload
- visit root_path
- forgot_password(user)
-
- expect(page).to have_content(I18n.t('devise.passwords.recently_reset'))
- expect(current_path).to eq new_user_password_path
+ expect{ forgot_password(user) }.not_to change{ user.reset_password_sent_at }
+ expect(page).to have_content(I18n.t('devise.passwords.send_paranoid_instructions'))
+ expect(current_path).to eq new_user_session_path
end
end
def forgot_password(user)
+ visit root_path
click_on 'Forgot your password?'
fill_in 'Email', with: user.email
click_button 'Reset password'
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 09fcff2444a..9a01c89ae2a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -70,6 +70,22 @@ 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
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index 06adb7633b2..d97831aae14 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -8,19 +8,29 @@ describe "Runners" do
describe "specific runners" do
before do
- @project = FactoryGirl.create :ci_project
- @project.gl_project.team << [user, :master]
+ @project = FactoryGirl.create :empty_project, shared_runners_enabled: false
+ @project.team << [user, :master]
- @project2 = FactoryGirl.create :ci_project
- @project2.gl_project.team << [user, :master]
+ @project2 = FactoryGirl.create :empty_project
+ @project2.team << [user, :master]
+
+ @project3 = FactoryGirl.create :empty_project
+ @project3.team << [user, :developer]
@shared_runner = FactoryGirl.create :ci_shared_runner
@specific_runner = FactoryGirl.create :ci_specific_runner
@specific_runner2 = FactoryGirl.create :ci_specific_runner
+ @specific_runner3 = FactoryGirl.create :ci_specific_runner
@project.runners << @specific_runner
@project2.runners << @specific_runner2
+ @project3.runners << @specific_runner3
+
+ visit runners_path(@project)
+ end
- visit runners_path(@project.gl_project)
+ before do
+ expect(page).to_not have_content(@specific_runner3.display_name)
+ expect(page).to_not have_content(@specific_runner3.display_name)
end
it "places runners in right places" do
@@ -39,7 +49,7 @@ describe "Runners" do
it "disables specific runner for project" do
@project2.runners << @specific_runner
- visit runners_path(@project.gl_project)
+ visit runners_path(@project)
within ".activated-specific-runners" do
click_on "Disable for this project"
@@ -59,9 +69,9 @@ describe "Runners" do
describe "shared runners" do
before do
- @project = FactoryGirl.create :ci_project
- @project.gl_project.team << [user, :master]
- visit runners_path(@project.gl_project)
+ @project = FactoryGirl.create :empty_project, shared_runners_enabled: false
+ @project.team << [user, :master]
+ visit runners_path(@project)
end
it "enables shared runners" do
@@ -72,14 +82,14 @@ describe "Runners" do
describe "show page" do
before do
- @project = FactoryGirl.create :ci_project
- @project.gl_project.team << [user, :master]
+ @project = FactoryGirl.create :empty_project
+ @project.team << [user, :master]
@specific_runner = FactoryGirl.create :ci_specific_runner
@project.runners << @specific_runner
- visit runners_path(@project.gl_project)
end
it "shows runner information" do
+ visit runners_path(@project)
click_on @specific_runner.short_sha
expect(page).to have_content(@specific_runner.platform)
end
diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb
index 4b78e3a61f0..65f8073c693 100644
--- a/spec/features/security/group_access_spec.rb
+++ b/spec/features/security/group_access_spec.rb
@@ -16,11 +16,11 @@ describe 'Group access', feature: true do
end
end
- def group_member(access_level, group = group)
+ def group_member(access_level, grp = group())
level = Object.const_get("Gitlab::Access::#{access_level.upcase}")
create(:user).tap do |user|
- group.add_user(user, level)
+ grp.add_user(user, level)
end
end
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index fca3c77fc64..b7368cca29d 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -47,7 +47,7 @@ feature 'Task Lists', feature: true do
it 'contains the required selectors' do
visit_issue(project, issue)
- container = '.issue-details .description.js-task-list-container'
+ container = '.detail-page-description .description.js-task-list-container'
expect(page).to have_selector(container)
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
@@ -123,7 +123,7 @@ feature 'Task Lists', feature: true do
it 'contains the required selectors' do
visit_merge_request(project, merge)
- container = '.merge-request-details .description.js-task-list-container'
+ container = '.detail-page-description .description.js-task-list-container'
expect(page).to have_selector(container)
expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 69492d58878..3cbc8253ad6 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -5,10 +5,9 @@ describe 'Triggers' do
before { login_as(user) }
before do
- @project = FactoryGirl.create :ci_project
- @gl_project = @project.gl_project
- @gl_project.team << [user, :master]
- visit namespace_project_triggers_path(@gl_project.namespace, @gl_project)
+ @project = FactoryGirl.create :empty_project
+ @project.team << [user, :master]
+ visit namespace_project_triggers_path(@project.namespace, @project)
end
context 'create a trigger' do
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index adb602f3edd..afea1840cd7 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -6,13 +6,12 @@ describe "Variables" do
describe "specific runners" do
before do
- @project = FactoryGirl.create :ci_project
- @gl_project = @project.gl_project
- @gl_project.team << [user, :master]
+ @project = FactoryGirl.create :empty_project
+ @project.team << [user, :master]
end
it "creates variable", js: true do
- visit namespace_project_variables_path(@gl_project.namespace, @gl_project)
+ 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"
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
new file mode 100644
index 00000000000..65d7f14c721
--- /dev/null
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe ContributedProjectsFinder do
+ let(:source_user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ let(:finder) { described_class.new(source_user) }
+
+ let!(:public_project) { create(:project, :public) }
+ let!(:private_project) { create(:project, :private) }
+
+ before do
+ private_project.team << [source_user, Gitlab::Access::MASTER]
+ private_project.team << [current_user, Gitlab::Access::DEVELOPER]
+ public_project.team << [source_user, Gitlab::Access::MASTER]
+
+ create(:event, action: Event::PUSHED, project: public_project,
+ target: public_project, author: source_user)
+
+ create(:event, action: Event::PUSHED, project: private_project,
+ target: private_project, author: source_user)
+ end
+
+ describe 'without a current user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'with a current user' do
+ subject { finder.execute(current_user) }
+
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+end
diff --git a/spec/finders/personal_projects_finder_spec.rb b/spec/finders/personal_projects_finder_spec.rb
new file mode 100644
index 00000000000..38817add456
--- /dev/null
+++ b/spec/finders/personal_projects_finder_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe PersonalProjectsFinder do
+ let(:source_user) { create(:user) }
+ let(:current_user) { create(:user) }
+
+ let(:finder) { described_class.new(source_user) }
+
+ let!(:public_project) do
+ create(:project, :public, namespace: source_user.namespace, name: 'A',
+ path: 'A')
+ end
+
+ let!(:private_project) do
+ create(:project, :private, namespace: source_user.namespace, name: 'B',
+ path: 'B')
+ end
+
+ before do
+ private_project.team << [current_user, Gitlab::Access::DEVELOPER]
+ end
+
+ describe 'without a current user' do
+ subject { finder.execute }
+
+ it { is_expected.to eq([public_project]) }
+ end
+
+ describe 'with a current user' do
+ subject { finder.execute(current_user) }
+
+ it { is_expected.to eq([private_project, public_project]) }
+ end
+end
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index de9d4cd6128..f32641ef0f6 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -1,51 +1,63 @@
require 'spec_helper'
describe ProjectsFinder do
- let(:user) { create :user }
- let(:group) { create :group }
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
- let(:project1) { create(:empty_project, :public, group: group) }
- let(:project2) { create(:empty_project, :internal, group: group) }
- let(:project3) { create(:empty_project, :private, group: group) }
- let(:project4) { create(:empty_project, :private, group: group) }
+ let!(:private_project) do
+ create(:project, :private, name: 'A', path: 'A')
+ end
- context 'non authenticated' do
- subject { ProjectsFinder.new.execute(nil, group: group) }
+ let!(:internal_project) do
+ create(:project, :internal, group: group, name: 'B', path: 'B')
+ end
- it { is_expected.to include(project1) }
- it { is_expected.not_to include(project2) }
- it { is_expected.not_to include(project3) }
- it { is_expected.not_to include(project4) }
- end
+ let!(:public_project) do
+ create(:project, :public, group: group, name: 'C', path: 'C')
+ end
- context 'authenticated' do
- subject { ProjectsFinder.new.execute(user, group: group) }
+ let(:finder) { described_class.new }
- it { is_expected.to include(project1) }
- it { is_expected.to include(project2) }
- it { is_expected.not_to include(project3) }
- it { is_expected.not_to include(project4) }
- end
+ describe 'without a group' do
+ describe 'without a user' do
+ subject { finder.execute }
- context 'authenticated, project member' do
- before { project3.team << [user, :developer] }
+ it { is_expected.to eq([public_project]) }
+ end
- subject { ProjectsFinder.new.execute(user, group: group) }
+ describe 'with a user' do
+ subject { finder.execute(user) }
- it { is_expected.to include(project1) }
- it { is_expected.to include(project2) }
- it { is_expected.to include(project3) }
- it { is_expected.not_to include(project4) }
- end
+ describe 'without private projects' do
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+
+ describe 'with private projects' do
+ before do
+ private_project.team.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it do
+ is_expected.to eq([public_project, internal_project,
+ private_project])
+ end
+ end
+ end
+ end
+
+ describe 'with a group' do
+ describe 'without a user' do
+ subject { finder.execute(nil, group: group) }
- context 'authenticated, group member' do
- before { group.add_developer(user) }
+ it { is_expected.to eq([public_project]) }
+ end
- subject { ProjectsFinder.new.execute(user, group: group) }
+ describe 'with a user' do
+ subject { finder.execute(user, group: group) }
- it { is_expected.to include(project1) }
- it { is_expected.to include(project2) }
- it { is_expected.to include(project3) }
- it { is_expected.to include(project4) }
+ it { is_expected.to eq([public_project, internal_project]) }
+ end
+ end
end
end
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index 41d12afa9ce..e8dfc5c0eb1 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -153,6 +153,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Ignores invalid: <%= User.reference_prefix %>fake_user
- Ignored in code: `<%= user.to_reference %>`
- Ignored in links: [Link to <%= user.to_reference %>](#user-link)
+- Link to user by reference: [User](<%= user.to_reference %>)
#### IssueReferenceFilter
@@ -160,6 +161,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Issue in another project: <%= xissue.to_reference(project) %>
- Ignored in code: `<%= issue.to_reference %>`
- Ignored in links: [Link to <%= issue.to_reference %>](#issue-link)
+- Issue by URL: <%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %>
+- Link to issue by reference: [Issue](<%= issue.to_reference %>)
+- Link to issue by URL: [Issue](<%= urls.namespace_project_issue_url(issue.project.namespace, issue.project, issue) %>)
#### MergeRequestReferenceFilter
@@ -167,6 +171,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Merge request in another project: <%= xmerge_request.to_reference(project) %>
- Ignored in code: `<%= merge_request.to_reference %>`
- Ignored in links: [Link to <%= merge_request.to_reference %>](#merge-request-link)
+- Merge request by URL: <%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %>
+- Link to merge request by reference: [Merge request](<%= merge_request.to_reference %>)
+- Link to merge request by URL: [Merge request](<%= urls.namespace_project_merge_request_url(merge_request.project.namespace, merge_request.project, merge_request) %>)
#### SnippetReferenceFilter
@@ -174,6 +181,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Snippet in another project: <%= xsnippet.to_reference(project) %>
- Ignored in code: `<%= snippet.to_reference %>`
- Ignored in links: [Link to <%= snippet.to_reference %>](#snippet-link)
+- Snippet by URL: <%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %>
+- Link to snippet by reference: [Snippet](<%= snippet.to_reference %>)
+- Link to snippet by URL: [Snippet](<%= urls.namespace_project_snippet_url(snippet.project.namespace, snippet.project, snippet) %>)
#### CommitRangeReferenceFilter
@@ -181,6 +191,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Range in another project: <%= xcommit_range.to_reference(project) %>
- Ignored in code: `<%= commit_range.to_reference %>`
- Ignored in links: [Link to <%= commit_range.to_reference %>](#commit-range-link)
+- Range by URL: <%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %>
+- Link to range by reference: [Range](<%= commit_range.to_reference %>)
+- Link to range by URL: [Range](<%= urls.namespace_project_compare_url(commit_range.project.namespace, commit_range.project, commit_range.to_param) %>)
#### CommitReferenceFilter
@@ -188,6 +201,9 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Commit in another project: <%= xcommit.to_reference(project) %>
- Ignored in code: `<%= commit.to_reference %>`
- Ignored in links: [Link to <%= commit.to_reference %>](#commit-link)
+- Commit by URL: <%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %>
+- Link to commit by reference: [Commit](<%= commit.to_reference %>)
+- Link to commit by URL: [Commit](<%= urls.namespace_project_commit_url(commit.project.namespace, commit.project, commit) %>)
#### LabelReferenceFilter
@@ -196,6 +212,7 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Label by name in quotes: <%= label.to_reference(:name) %>
- Ignored in code: `<%= simple_label.to_reference %>`
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
+- Link to label by reference: [Label](<%= label.to_reference %>)
### Task Lists
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 1dfae0fbd3f..68527c3a4f8 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -59,7 +59,7 @@ describe ApplicationHelper do
avatar_url = "http://localhost/uploads/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
- to eq "<img alt=\"Banana sample\" src=\"#{avatar_url}\" />"
+ to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
end
it 'should give uploaded icon when present' do
@@ -95,9 +95,9 @@ describe ApplicationHelper do
end
it 'should call gravatar_icon when no User exists with the given email' do
- expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20)
+ expect(helper).to receive(:gravatar_icon).with('foo@example.com', 20, 2)
- helper.avatar_icon('foo@example.com', 20)
+ helper.avatar_icon('foo@example.com', 20, 2)
end
describe 'using a User' do
@@ -150,15 +150,19 @@ describe ApplicationHelper do
stub_gravatar_setting(plain_url: 'http://example.local/?s=%{size}&hash=%{hash}')
expect(gravatar_icon(user_email, 20)).
- to eq('http://example.local/?s=20&hash=b58c6f14d292556214bd64909bcdb118')
+ to eq('http://example.local/?s=40&hash=b58c6f14d292556214bd64909bcdb118')
end
it 'accepts a custom size argument' do
- expect(helper.gravatar_icon(user_email, 64)).to include '?s=64'
+ expect(helper.gravatar_icon(user_email, 64)).to include '?s=128'
end
- it 'defaults size to 40 when given an invalid size' do
- expect(helper.gravatar_icon(user_email, nil)).to include '?s=40'
+ it 'defaults size to 40@2x when given an invalid size' do
+ expect(helper.gravatar_icon(user_email, nil)).to include '?s=80'
+ end
+
+ it 'accepts a scaling factor' do
+ expect(helper.gravatar_icon(user_email, 40, 3)).to include '?s=120'
end
it 'ignores case and surrounding whitespace' do
@@ -259,11 +263,12 @@ describe ApplicationHelper do
end
it 'includes a default js-timeago class' do
- expect(element.attr('class')).to eq 'time_ago js-timeago'
+ expect(element.attr('class')).to eq 'time_ago js-timeago js-timeago-pending'
end
it 'accepts a custom html_class' do
- expect(element(html_class: 'custom_class').attr('class')).to eq 'custom_class js-timeago'
+ expect(element(html_class: 'custom_class').attr('class')).
+ to eq 'custom_class js-timeago js-timeago-pending'
end
it 'accepts a custom tooltip placement' do
@@ -274,7 +279,7 @@ describe ApplicationHelper do
el = element.next_element
expect(el.name).to eq 'script'
- expect(el.text).to include "$('.js-timeago').timeago()"
+ expect(el.text).to include "$('.js-timeago-pending').removeClass('js-timeago-pending').timeago()"
end
it 'allows the script tag to be excluded' do
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
index 7fc53eb1472..4f8d9c67262 100644
--- a/spec/helpers/ci_status_helper_spec.rb
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -6,13 +6,8 @@ describe CiStatusHelper do
let(:success_commit) { double("Ci::Commit", status: 'success') }
let(:failed_commit) { double("Ci::Commit", status: 'failed') }
- describe 'ci_status_color' do
- it { expect(ci_status_icon(success_commit)).to include('fa-check') }
- it { expect(ci_status_icon(failed_commit)).to include('fa-close') }
- end
-
- describe 'ci_status_color' do
- it { expect(ci_status_color(success_commit)).to eq('green') }
- it { expect(ci_status_color(failed_commit)).to eq('red') }
+ describe 'ci_status_icon' do
+ it { expect(helper.ci_status_icon(success_commit)).to include('fa-check') }
+ it { expect(helper.ci_status_icon(failed_commit)).to include('fa-close') }
end
end
diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper.rb
index 5d174460681..4ea90a80a92 100644
--- a/spec/helpers/groups_helper.rb
+++ b/spec/helpers/groups_helper.rb
@@ -9,7 +9,7 @@ describe GroupsHelper do
group.avatar = File.open(avatar_file_path)
group.save!
expect(group_icon(group.path).to_s).
- to match("/uploads/group/avatar/#{ group.id }/banana_sample.gif")
+ to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
end
it 'should give default avatar_icon when no avatar is present' do
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 78a6b631eb2..ffd8ebae029 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -127,4 +127,25 @@ 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
+
+ it "returns empty string for unauthenticated user" do
+ expect(note_active_class(Note.all, nil)).to eq("")
+ end
+
+ it "returns active string for author" do
+ expect(note_active_class(Note.all, @note.author)).to eq("active")
+ end
+ end
+
+ describe "#awards_sort" do
+ it "sorts a hash so thumbsup and thumbsdown are always on top" do
+ data = { "thumbsdown" => "some value", "lifter" => "some value", "thumbsup" => "some value" }
+ expect(awards_sort(data).keys).to eq(["thumbsup", "thumbsdown", "lifter"])
+ end
+ end
end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index 0ef1efb8bce..600e1c4e9ec 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -1,24 +1,57 @@
require 'spec_helper'
describe MergeRequestsHelper do
- describe "#issues_sentence" do
+ describe 'ci_build_details_path' do
+ let(:project) { create :project }
+ let(:merge_request) { MergeRequest.new }
+ let(:ci_service) { CiService.new }
+ let(:last_commit) { Ci::Commit.new({}) }
+
+ before do
+ allow(merge_request).to receive(:source_project).and_return(project)
+ allow(merge_request).to receive(:last_commit).and_return(last_commit)
+ allow(project).to receive(:ci_service).and_return(ci_service)
+ allow(last_commit).to receive(:sha).and_return('12d65c')
+ end
+
+ 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")
+ end
+ end
+
+ describe '#issues_sentence' do
subject { issues_sentence(issues) }
let(:issues) do
[build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
end
it { is_expected.to eq('#1, #2, and #3') }
+
+ context 'for JIRA issues' do
+ let(:project) { create(:project) }
+ let(:issues) do
+ [
+ JiraIssue.new('JIRA-123', project),
+ JiraIssue.new('JIRA-456', project),
+ JiraIssue.new('FOOBAR-7890', project)
+ ]
+ end
+
+ it { is_expected.to eq('FOOBAR-7890, JIRA-123, and JIRA-456') }
+ end
end
- describe "#format_mr_branch_names" do
- describe "within the same project" do
+ describe '#format_mr_branch_names' do
+ describe 'within the same project' do
let(:merge_request) { create(:merge_request) }
subject { format_mr_branch_names(merge_request) }
it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) }
end
- describe "within different projects" do
+ describe 'within different projects' do
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
new file mode 100644
index 00000000000..fd7107779f6
--- /dev/null
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -0,0 +1,129 @@
+require 'rails_helper'
+
+describe PageLayoutHelper do
+ describe 'page_description' do
+ it 'defaults to value returned by page_description_default helper' do
+ allow(helper).to receive(:page_description_default).and_return('Foo')
+
+ expect(helper.page_description).to eq 'Foo'
+ end
+
+ it 'returns the last-pushed description' do
+ helper.page_description('Foo')
+ helper.page_description('Bar')
+ helper.page_description('Baz')
+
+ expect(helper.page_description).to eq 'Baz'
+ end
+
+ it 'squishes multiple newlines' do
+ helper.page_description("Foo\nBar\nBaz")
+
+ expect(helper.page_description).to eq 'Foo Bar Baz'
+ end
+
+ it 'truncates' do
+ helper.page_description <<-LOREM.strip_heredoc
+ Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo
+ ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis
+ dis parturient montes, nascetur ridiculus mus. Donec quam felis,
+ ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa
+ quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget,
+ arcu.
+ LOREM
+
+ expect(helper.page_description).to end_with 'quam felis,...'
+ end
+
+ it 'sanitizes all HTML' do
+ helper.page_description("<b>Bold</b> <h1>Header</h1>")
+
+ expect(helper.page_description).to eq 'Bold Header'
+ end
+ end
+
+ describe 'page_description_default' do
+ it 'uses Project description when available' do
+ project = double(description: 'Project Description')
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper.page_description_default).to eq 'Project Description'
+ end
+
+ it 'uses brand_title when Project description is nil' do
+ project = double(description: nil)
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper).to receive(:brand_title).and_return('Brand Title')
+ expect(helper.page_description_default).to eq 'Brand Title'
+ end
+
+ it 'falls back to brand_title' do
+ allow(helper).to receive(:brand_title).and_return('Brand Title')
+
+ expect(helper.page_description_default).to eq 'Brand Title'
+ end
+ end
+
+ describe 'page_image' do
+ it 'defaults to the GitLab logo' do
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
+
+ context 'with @project' do
+ it 'uses Project avatar if available' do
+ project = double(avatar_url: 'http://example.com/uploads/avatar.png')
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper.page_image).to eq project.avatar_url
+ end
+
+ it 'falls back to the default' do
+ project = double(avatar_url: nil)
+ helper.instance_variable_set(:@project, project)
+
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
+ end
+
+ context 'with @user' do
+ it 'delegates to avatar_icon helper' do
+ user = double('User')
+ helper.instance_variable_set(:@user, user)
+
+ expect(helper).to receive(:avatar_icon).with(user)
+
+ helper.page_image
+ end
+ end
+ end
+
+ describe 'page_card_attributes' do
+ it 'raises ArgumentError when given more than two attributes' do
+ map = { foo: 'foo', bar: 'bar', baz: 'baz' }
+
+ expect { helper.page_card_attributes(map) }.
+ to raise_error(ArgumentError, /more than two attributes/)
+ end
+
+ it 'rejects blank values' do
+ map = { foo: 'foo', bar: '' }
+ helper.page_card_attributes(map)
+
+ expect(helper.page_card_attributes).to eq({ foo: 'foo' })
+ end
+ end
+
+ describe 'page_card_meta_tags' do
+ it 'returns the twitter:label and twitter:data tags' do
+ allow(helper).to receive(:page_card_attributes).and_return(foo: 'bar')
+
+ tags = helper.page_card_meta_tags
+
+ aggregate_failures do
+ expect(tags).to include %q(<meta property="twitter:label1" content="foo" />)
+ expect(tags).to include %q(<meta property="twitter:data1" content="bar" />)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index f2efb528aeb..53207767581 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -53,6 +53,16 @@ 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) }
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index b327f4f911a..f0d553f5f1d 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -42,6 +42,11 @@ describe SearchHelper do
expect(search_autocomplete_opts(project.name).size).to eq(1)
end
+ it "includes the public group" do
+ group = create(:group)
+ expect(search_autocomplete_opts(group.name).size).to eq(1)
+ end
+
context "with a current project" do
before { @project = create(:project) }
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
index c4f7693329c..aafc24397a9 100644
--- a/spec/helpers/visibility_level_helper_spec.rb
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -7,69 +7,52 @@ describe VisibilityLevelHelper do
init_haml_helpers
end
- let(:project) { create(:project) }
+ let(:project) { build(:project) }
+ let(:personal_snippet) { build(:personal_snippet) }
+ let(:project_snippet) { build(:project_snippet) }
describe 'visibility_level_description' do
- shared_examples 'a visibility level description' do
- let(:desc) do
- visibility_level_description(Gitlab::VisibilityLevel::PRIVATE,
- form_model)
- end
-
- let(:expected_class) do
- class_name = case form_model.class.name
- when 'String'
- form_model
- else
- form_model.class.name
- end
-
- class_name.match(/(project|snippet)$/i)[0]
- end
-
- it 'should refer to the correct class' do
- expect(desc).to match(/#{expected_class}/i)
+ context 'used with a Project' do
+ it 'delegates projects to #project_visibility_level_description' do
+ expect(visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, project))
+ .to match /project/i
end
end
- context 'form_model argument is a String' do
- context 'model object is a personal snippet' do
- it_behaves_like 'a visibility level description' do
- let(:form_model) { 'PersonalSnippet' }
- end
+ context 'called with a Snippet' do
+ it 'delegates snippets to #snippet_visibility_level_description' do
+ expect(visibility_level_description(Gitlab::VisibilityLevel::INTERNAL, project_snippet))
+ .to match /snippet/i
end
+ end
+ end
- context 'model object is a project snippet' do
- it_behaves_like 'a visibility level description' do
- let(:form_model) { 'ProjectSnippet' }
- end
- end
+ describe "#project_visibility_level_description" do
+ it "describes private projects" do
+ expect(project_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE))
+ .to eq "Project access must be granted explicitly to each user."
+ end
- context 'model object is a project' do
- it_behaves_like 'a visibility level description' do
- let(:form_model) { 'Project' }
- end
- end
+ it "describes public projects" do
+ expect(project_visibility_level_description(Gitlab::VisibilityLevel::PUBLIC))
+ .to eq "The project can be cloned without any authentication."
end
+ end
- context 'form_model argument is a model object' do
- context 'model object is a personal snippet' do
- it_behaves_like 'a visibility level description' do
- let(:form_model) { create(:personal_snippet) }
- end
- end
+ describe "#snippet_visibility_level_description" do
+ it 'describes visibility only for me' do
+ expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, personal_snippet))
+ .to eq "The snippet is visible only to me."
+ end
- context 'model object is a project snippet' do
- it_behaves_like 'a visibility level description' do
- let(:form_model) { create(:project_snippet, project: project) }
- end
- end
+ it 'describes visibility for project members' do
+ expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE, project_snippet))
+ .to eq "The snippet is visible only to project members."
+ end
- context 'model object is a project' do
- it_behaves_like 'a visibility level description' do
- let(:form_model) { project }
- end
- end
+ it 'defaults to personal snippet' do
+ expect(snippet_visibility_level_description(Gitlab::VisibilityLevel::PRIVATE))
+ .to eq "The snippet is visible only to me."
end
end
diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml
index 7e8b2a64351..470cabeafbb 100644
--- a/spec/javascripts/fixtures/issues_show.html.haml
+++ b/spec/javascripts/fixtures/issues_show.html.haml
@@ -1,6 +1,16 @@
-%a.btn-close
+:css
+ .hidden { display: none !important; }
-.issue-details
+.flash-container
+ .flash-alert
+ .flash-notice
+
+.status-box.status-box-open Open
+.status-box.status-box-closed.hidden Closed
+%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close
+%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen
+
+.detail-page-description
.description.js-task-list-container
.wiki
%ul.task-list
diff --git a/spec/javascripts/fixtures/merge_request_tabs.html.haml b/spec/javascripts/fixtures/merge_request_tabs.html.haml
index 7624a713948..68678c3d7e3 100644
--- a/spec/javascripts/fixtures/merge_request_tabs.html.haml
+++ b/spec/javascripts/fixtures/merge_request_tabs.html.haml
@@ -1,12 +1,12 @@
%ul.nav.nav-tabs.merge-request-tabs
%li.notes-tab
- %a{href: '/foo/bar/merge_requests/1', data: {target: '#notes', action: 'notes', toggle: 'tab'}}
+ %a{href: '/foo/bar/merge_requests/1', data: {target: 'div#notes', action: 'notes', toggle: 'tab'}}
Discussion
%li.commits-tab
- %a{href: '/foo/bar/merge_requests/1/commits', data: {target: '#commits', action: 'commits', toggle: 'tab'}}
+ %a{href: '/foo/bar/merge_requests/1/commits', data: {target: 'div#commits', action: 'commits', toggle: 'tab'}}
Commits
%li.diffs-tab
- %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: '#diffs', action: 'diffs', toggle: 'tab'}}
+ %a{href: '/foo/bar/merge_requests/1/diffs', data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'}}
Diffs
.tab-content
diff --git a/spec/javascripts/fixtures/merge_requests_show.html.haml b/spec/javascripts/fixtures/merge_requests_show.html.haml
index f0c622935f8..8447dfdda32 100644
--- a/spec/javascripts/fixtures/merge_requests_show.html.haml
+++ b/spec/javascripts/fixtures/merge_requests_show.html.haml
@@ -1,6 +1,6 @@
%a.btn-close
-.merge-request-details
+.detail-page-description
.description.js-task-list-container
.wiki
%ul.task-list
diff --git a/spec/javascripts/fixtures/new_branch.html.haml b/spec/javascripts/fixtures/new_branch.html.haml
new file mode 100644
index 00000000000..f06629e5ecc
--- /dev/null
+++ b/spec/javascripts/fixtures/new_branch.html.haml
@@ -0,0 +1,4 @@
+%form.js-create-branch-form
+ %input.js-branch-name
+ .js-branch-name-error
+ %input{id: "ref"}
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index 268e4c68c31..7e67c778861 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -20,3 +20,89 @@ describe 'Issue', ->
expect(req.data.issue.description).not.toBe(null)
$('.js-task-list-field').trigger('tasklist:changed')
+describe 'reopen/close issue', ->
+ fixture.preload('issues_show.html')
+ beforeEach ->
+ fixture.load('issues_show.html')
+ @issue = new Issue()
+ it 'closes an issue', ->
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://gitlab.com/issues/6/close')
+ obj.success saved: true
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ expect($btnReopen).toBeHidden()
+ expect($btnClose.text()).toBe('Close')
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined')
+
+ $btnClose.trigger('click')
+
+ expect($btnReopen).toBeVisible()
+ expect($btnClose).toBeHidden()
+ expect($('div.status-box-closed')).toBeVisible()
+ expect($('div.status-box-open')).toBeHidden()
+
+ it 'fails to closes an issue with success:false', ->
+
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
+ obj.success saved: false
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ $btnClose.attr('href','http://goesnowhere.nothing/whereami')
+ expect($btnReopen).toBeHidden()
+ expect($btnClose.text()).toBe('Close')
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined')
+
+ $btnClose.trigger('click')
+
+ expect($btnReopen).toBeHidden()
+ expect($btnClose).toBeVisible()
+ expect($('div.status-box-closed')).toBeHidden()
+ expect($('div.status-box-open')).toBeVisible()
+ expect($('div.flash-alert')).toBeVisible()
+ expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
+
+ it 'fails to closes an issue with HTTP error', ->
+
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
+ obj.error()
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ $btnClose.attr('href','http://goesnowhere.nothing/whereami')
+ expect($btnReopen).toBeHidden()
+ expect($btnClose.text()).toBe('Close')
+ expect(typeof $btnClose.prop('disabled')).toBe('undefined')
+
+ $btnClose.trigger('click')
+
+ expect($btnReopen).toBeHidden()
+ expect($btnClose).toBeVisible()
+ expect($('div.status-box-closed')).toBeHidden()
+ expect($('div.status-box-open')).toBeVisible()
+ expect($('div.flash-alert')).toBeVisible()
+ expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
+
+ it 'reopens an issue', ->
+ $.ajax = (obj) ->
+ expect(obj.type).toBe('PUT')
+ expect(obj.url).toBe('http://gitlab.com/issues/6/reopen')
+ obj.success saved: true
+
+ $btnClose = $('a.btn-close')
+ $btnReopen = $('a.btn-reopen')
+ expect($btnReopen.text()).toBe('Reopen')
+
+ $btnReopen.trigger('click')
+
+ expect($btnReopen).toBeHidden()
+ expect($btnClose).toBeVisible()
+ expect($('div.status-box-open')).toBeVisible()
+ expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file
diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee
new file mode 100644
index 00000000000..f2ce85efcdc
--- /dev/null
+++ b/spec/javascripts/new_branch_spec.js.coffee
@@ -0,0 +1,160 @@
+#= require jquery-ui
+#= require new_branch_form
+
+describe 'Branch', ->
+ describe 'create a new branch', ->
+ fixture.preload('new_branch.html')
+
+ fillNameWith = (value) ->
+ $('.js-branch-name').val(value).trigger('blur')
+
+ expectToHaveError = (error) ->
+ expect($('.js-branch-name-error span').text()).toEqual(error)
+
+ beforeEach ->
+ fixture.load('new_branch.html')
+ $('form').on 'submit', (e) -> e.preventDefault()
+
+ @form = new NewBranchForm($('.js-create-branch-form'), [])
+
+ it "can't start with a dot", ->
+ fillNameWith '.foo'
+ expectToHaveError "can't start with '.'"
+
+ it "can't start with a slash", ->
+ fillNameWith '/foo'
+ expectToHaveError "can't start with '/'"
+
+ it "can't have two consecutive dots", ->
+ fillNameWith 'foo..bar'
+ expectToHaveError "can't contain '..'"
+
+ it "can't have spaces anywhere", ->
+ fillNameWith ' foo'
+ expectToHaveError "can't contain spaces"
+ fillNameWith 'foo bar'
+ expectToHaveError "can't contain spaces"
+ fillNameWith 'foo '
+ expectToHaveError "can't contain spaces"
+
+ it "can't have ~ anywhere", ->
+ fillNameWith '~foo'
+ expectToHaveError "can't contain '~'"
+ fillNameWith 'foo~bar'
+ expectToHaveError "can't contain '~'"
+ fillNameWith 'foo~'
+ expectToHaveError "can't contain '~'"
+
+ it "can't have tilde anwhere", ->
+ fillNameWith '~foo'
+ expectToHaveError "can't contain '~'"
+ fillNameWith 'foo~bar'
+ expectToHaveError "can't contain '~'"
+ fillNameWith 'foo~'
+ expectToHaveError "can't contain '~'"
+
+ it "can't have caret anywhere", ->
+ fillNameWith '^foo'
+ expectToHaveError "can't contain '^'"
+ fillNameWith 'foo^bar'
+ expectToHaveError "can't contain '^'"
+ fillNameWith 'foo^'
+ expectToHaveError "can't contain '^'"
+
+ it "can't have : anywhere", ->
+ fillNameWith ':foo'
+ expectToHaveError "can't contain ':'"
+ fillNameWith 'foo:bar'
+ expectToHaveError "can't contain ':'"
+ fillNameWith ':foo'
+ expectToHaveError "can't contain ':'"
+
+ it "can't have question mark anywhere", ->
+ fillNameWith '?foo'
+ expectToHaveError "can't contain '?'"
+ fillNameWith 'foo?bar'
+ expectToHaveError "can't contain '?'"
+ fillNameWith 'foo?'
+ expectToHaveError "can't contain '?'"
+
+ it "can't have asterisk anywhere", ->
+ fillNameWith '*foo'
+ expectToHaveError "can't contain '*'"
+ fillNameWith 'foo*bar'
+ expectToHaveError "can't contain '*'"
+ fillNameWith 'foo*'
+ expectToHaveError "can't contain '*'"
+
+ it "can't have open bracket anywhere", ->
+ fillNameWith '[foo'
+ expectToHaveError "can't contain '['"
+ fillNameWith 'foo[bar'
+ expectToHaveError "can't contain '['"
+ fillNameWith 'foo['
+ expectToHaveError "can't contain '['"
+
+ it "can't have a backslash anywhere", ->
+ fillNameWith '\\foo'
+ expectToHaveError "can't contain '\\'"
+ fillNameWith 'foo\\bar'
+ expectToHaveError "can't contain '\\'"
+ fillNameWith 'foo\\'
+ expectToHaveError "can't contain '\\'"
+
+ it "can't contain a sequence @{ anywhere", ->
+ fillNameWith '@{foo'
+ expectToHaveError "can't contain '@{'"
+ fillNameWith 'foo@{bar'
+ expectToHaveError "can't contain '@{'"
+ fillNameWith 'foo@{'
+ expectToHaveError "can't contain '@{'"
+
+ it "can't have consecutive slashes", ->
+ fillNameWith 'foo//bar'
+ expectToHaveError "can't contain consecutive slashes"
+
+ it "can't end with a slash", ->
+ fillNameWith 'foo/'
+ expectToHaveError "can't end in '/'"
+
+ it "can't end with a dot", ->
+ fillNameWith 'foo.'
+ expectToHaveError "can't end in '.'"
+
+ it "can't end with .lock", ->
+ fillNameWith 'foo.lock'
+ expectToHaveError "can't end in '.lock'"
+
+ it "can't be the single character @", ->
+ fillNameWith '@'
+ expectToHaveError "can't be '@'"
+
+ it "concatenates all error messages", ->
+ fillNameWith '/foo bar?~.'
+ expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'"
+
+ it "doesn't duplicate error messages", ->
+ fillNameWith '?foo?bar?zoo?'
+ expectToHaveError "can't contain '?'"
+
+ it "removes the error message when is a valid name", ->
+ fillNameWith 'foo?bar'
+ expect($('.js-branch-name-error span').length).toEqual(1)
+ fillNameWith 'foobar'
+ expect($('.js-branch-name-error span').length).toEqual(0)
+
+ it "can have dashes anywhere", ->
+ fillNameWith '-foo-bar-zoo-'
+ expect($('.js-branch-name-error span').length).toEqual(0)
+
+ it "can have underscores anywhere", ->
+ fillNameWith '_foo_bar_zoo_'
+ expect($('.js-branch-name-error span').length).toEqual(0)
+
+ it "can have numbers anywhere", ->
+ fillNameWith '1foo2bar3zoo4'
+ expect($('.js-branch-name-error span').length).toEqual(0)
+
+ it "can be only letters", ->
+ fillNameWith 'foo'
+ expect($('.js-branch-name-error span').length).toEqual(0)
diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb
new file mode 100644
index 00000000000..81b9a513ce3
--- /dev/null
+++ b/spec/lib/banzai/cross_project_reference_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Banzai::CrossProjectReference, lib: true do
+ include described_class
+
+ describe '#project_from_ref' do
+ context 'when no project was referenced' do
+ it 'returns the project from context' do
+ project = double
+
+ allow(self).to receive(:context).and_return({ project: project })
+
+ expect(project_from_ref(nil)).to eq project
+ end
+ end
+
+ context 'when referenced project does not exist' do
+ it 'returns nil' do
+ expect(project_from_ref('invalid/reference')).to be_nil
+ end
+ end
+
+ context 'when referenced project exists' do
+ it 'returns the referenced project' do
+ project2 = double('referenced project')
+
+ expect(Project).to receive(:find_with_namespace).
+ with('cross/reference').and_return(project2)
+
+ expect(project_from_ref('cross/reference')).to eq project2
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
new file mode 100644
index 00000000000..84c2ddf444e
--- /dev/null
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -0,0 +1,112 @@
+require 'spec_helper'
+
+describe Banzai::Filter::AutolinkFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:link) { 'http://about.gitlab.com/' }
+
+ it 'does nothing when :autolink is false' do
+ exp = act = link
+ expect(filter(act, autolink: false).to_html).to eq exp
+ end
+
+ it 'does nothing with non-link text' do
+ exp = act = 'This text contains no links to autolink'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ context 'Rinku schemes' do
+ it 'autolinks http' do
+ doc = filter("See #{link}")
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks https' do
+ link = 'https://google.com/'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks ftp' do
+ link = 'ftp://ftp.us.debian.org/debian/'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks short URLs' do
+ link = 'http://localhost:3000/'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'accepts link_attr options' do
+ doc = filter("See #{link}", link_attr: { class: 'custom' })
+
+ expect(doc.at_css('a')['class']).to eq 'custom'
+ end
+
+ described_class::IGNORE_PARENTS.each do |elem|
+ it "ignores valid links contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>See #{link}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+
+ context 'other schemes' do
+ let(:link) { 'foo://bar.baz/' }
+
+ it 'autolinks smb' do
+ link = 'smb:///Volumes/shared/foo.pdf'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks irc' do
+ link = 'irc://irc.freenode.net/git'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'does not include trailing punctuation' do
+ doc = filter("See #{link}.")
+ expect(doc.at_css('a').text).to eq link
+
+ doc = filter("See #{link}, ok?")
+ expect(doc.at_css('a').text).to eq link
+
+ doc = filter("See #{link}...")
+ expect(doc.at_css('a').text).to eq link
+ end
+
+ it 'does not include trailing HTML entities' do
+ doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
+
+ expect(doc.at_css('a')['href']).to eq link
+ expect(doc.text).to eq "See <<<#{link}>>>"
+ end
+
+ it 'accepts link_attr options' do
+ doc = filter("See #{link}", link_attr: { class: 'custom' })
+ expect(doc.at_css('a')['class']).to eq 'custom'
+ end
+
+ described_class::IGNORE_PARENTS.each do |elem|
+ it "ignores valid links contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>See #{link}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ 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
new file mode 100644
index 00000000000..c2a8ad36c30
--- /dev/null
+++ b/spec/lib/banzai/filter/commit_range_reference_filter_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe Banzai::Filter::CommitRangeReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project, :public) }
+ let(:commit1) { project.commit("HEAD~2") }
+ let(:commit2) { project.commit }
+
+ let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}", project) }
+ let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}", project) }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { range.to_reference }
+ let(:reference2) { range2.to_reference }
+
+ it 'links to a valid two-dot reference' do
+ doc = reference_filter("See #{reference2}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param)
+ end
+
+ it 'links to a valid three-dot reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param)
+ end
+
+ it 'links to a valid short ID' do
+ reference = "#{commit1.short_id}...#{commit2.id}"
+ reference2 = "#{commit1.id}...#{commit2.short_id}"
+
+ exp = commit1.short_id + '...' + commit2.short_id
+
+ expect(reference_filter("See #{reference}").css('a').first.text).to eq exp
+ expect(reference_filter("See #{reference2}").css('a').first.text).to eq exp
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+
+ exp = Regexp.escape(range.reference_link_text)
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs' do
+ exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
+
+ expect(project).to receive(:valid_repo?).and_return(true)
+ expect(project.repository).to receive(:commit).with(commit1.id.reverse)
+ expect(project.repository).to receive(:commit).with(commit2.id)
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq range.reference_title
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("See #{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-commit-range attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-commit-range')
+ expect(link.attr('data-commit-range')).to eq range.to_s
+ end
+
+ it 'supports an :only_path option' do
+ doc = reference_filter("See #{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_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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:reference) { range.to_reference(project) }
+
+ before do
+ range.project = project2
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ exp = Regexp.escape("#{project2.to_reference}@#{range.reference_link_text}")
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+ expect(reference_filter(act).to_html).to eq exp
+
+ 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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:range) { CommitRange.new("#{commit1.id}...master", project) }
+ let(:reference) { urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: 'master') }
+
+ before do
+ range.project = project2
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq reference
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ exp = Regexp.escape(range.reference_link_text(project))
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
+ expect(reference_filter(act).to_html).to eq exp
+
+ 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
new file mode 100644
index 00000000000..473534ba68a
--- /dev/null
+++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb
@@ -0,0 +1,163 @@
+require 'spec_helper'
+
+describe Banzai::Filter::CommitReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project, :public) }
+ let(:commit) { project.commit }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { commit.id }
+
+ # Let's test a variety of commit SHA sizes just to be paranoid
+ [6, 8, 12, 18, 20, 32, 40].each do |size|
+ it "links to a valid reference of #{size} characters" do
+ doc = reference_filter("See #{reference[0...size]}")
+
+ expect(doc.css('a').first.text).to eq commit.short_id
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project.namespace, project, reference)
+ end
+ end
+
+ it 'always uses the short ID as the link text' do
+ doc = reference_filter("See #{commit.id}")
+ expect(doc.text).to eq "See #{commit.short_id}"
+
+ doc = reference_filter("See #{commit.id[0...6]}")
+ expect(doc.text).to eq "See #{commit.short_id}"
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs' do
+ invalid = invalidate_reference(reference)
+ exp = act = "See #{invalid}"
+
+ expect(project).to receive(:valid_repo?).and_return(true)
+ expect(project.repository).to receive(:commit).with(invalid)
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq commit.link_title
+ end
+
+ it 'escapes the title attribute' do
+ allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
+
+ doc = reference_filter("See #{reference}")
+ expect(doc.text).to eq "See #{commit.short_id}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("See #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("See #{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-commit attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-commit')
+ expect(link.attr('data-commit')).to eq commit.id
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("See #{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_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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:commit) { project2.commit }
+ let(:reference) { commit.to_reference(project) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ exp = Regexp.escape(project2.to_reference)
+ expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' 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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:commit) { project2.commit }
+ let(:reference) { urls.namespace_project_commit_url(project2.namespace, project2, commit.id) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+
+ expect(doc.to_html).to match(/\(<a.+>#{commit.reference_link_text(project)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' 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/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb
new file mode 100644
index 00000000000..cf314058158
--- /dev/null
+++ b/spec/lib/banzai/filter/emoji_filter_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+describe Banzai::Filter::EmojiFilter, lib: true do
+ include FilterSpecHelper
+
+ before do
+ @original_asset_host = ActionController::Base.asset_host
+ ActionController::Base.asset_host = 'https://foo.com'
+ end
+
+ after do
+ ActionController::Base.asset_host = @original_asset_host
+ end
+
+ it 'replaces supported emoji' do
+ doc = filter('<p>:heart:</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
+ end
+
+ it 'ignores unsupported emoji' do
+ exp = act = '<p>:foo:</p>'
+ doc = filter(act)
+ expect(doc.to_html).to match Regexp.escape(exp)
+ end
+
+ it 'correctly encodes the URL' do
+ doc = filter('<p>:+1:</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
+ end
+
+ it 'matches at the start of a string' do
+ doc = filter(':+1:')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches at the end of a string' do
+ doc = filter('This gets a :-1:')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches with adjacent text' do
+ doc = filter('+1 (:+1:)')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches multiple emoji in a row' do
+ doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
+ expect(doc.css('img').size).to eq 3
+ end
+
+ it 'has a title attribute' do
+ doc = filter(':-1:')
+ expect(doc.css('img').first.attr('title')).to eq ':-1:'
+ end
+
+ it 'has an alt attribute' do
+ doc = filter(':-1:')
+ expect(doc.css('img').first.attr('alt')).to eq ':-1:'
+ end
+
+ it 'has an align attribute' do
+ doc = filter(':8ball:')
+ expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
+ end
+
+ it 'has an emoji class' do
+ doc = filter(':cat:')
+ expect(doc.css('img').first.attr('class')).to eq 'emoji'
+ end
+
+ it 'has height and width attributes' do
+ doc = filter(':dog:')
+ img = doc.css('img').first
+
+ expect(img.attr('width')).to eq '20'
+ expect(img.attr('height')).to eq '20'
+ end
+
+ it 'keeps whitespace intact' do
+ doc = filter('This deserves a :+1:, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ end
+
+ it 'uses a custom asset_root context' do
+ root = Gitlab.config.gitlab.url + 'gitlab/root'
+
+ doc = filter(':smile:', asset_root: root)
+ expect(doc.css('img').first.attr('src')).to start_with(root)
+ end
+
+ it 'uses a custom asset_host context' do
+ ActionController::Base.asset_host = 'https://cdn.example.com'
+
+ doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
+ expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ end
+end
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
new file mode 100644
index 00000000000..953466679e4
--- /dev/null
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -0,0 +1,77 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ def helper
+ IssuesHelper
+ end
+
+ let(:project) { create(:jira_project) }
+
+ context 'JIRA issue references' do
+ let(:issue) { ExternalIssue.new('JIRA-123', project) }
+ let(:reference) { issue.to_reference }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'ignores valid references when using default tracker' do
+ expect(project).to receive(:default_issues_tracker?).and_return(true)
+
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('href'))
+ .to eq helper.url_for_issue(reference, project)
+ end
+
+ it 'links to the external tracker' do
+ doc = filter("Issue #{reference}")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq "http://jira.example/browse/#{reference}"
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Issue (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
+ end
+
+ it 'escapes the title attribute' do
+ allow(project.external_issue_tracker).to receive(:title).
+ and_return(%{"></a>whatever<a title="})
+
+ doc = filter("Issue #{reference}")
+ expect(doc.text).to eq "Issue #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Issue #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
+ 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
new file mode 100644
index 00000000000..e3a8e15330e
--- /dev/null
+++ b/spec/lib/banzai/filter/external_link_filter_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ExternalLinkFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'ignores elements without an href attribute' do
+ exp = act = %q(<a id="ignored">Ignore Me</a>)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'ignores non-HTTP(S) links' do
+ exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'skips internal links' do
+ internal = Gitlab.config.gitlab.url
+ exp = act = %Q(<a href="#{internal}/sign_in">Login</a>)
+ 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 eq 'nofollow'
+ end
+end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
new file mode 100644
index 00000000000..5a0d3d577a8
--- /dev/null
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -0,0 +1,209 @@
+require 'spec_helper'
+
+describe Banzai::Filter::IssueReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ def helper
+ IssuesHelper
+ end
+
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' 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)
+
+ exp = act = "Issue #{reference}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("Fixed #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs' do
+ invalid = invalidate_reference(reference)
+ exp = act = "Fixed #{invalid}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
+ end
+
+ it 'escapes the title attribute' do
+ issue.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = reference_filter("Issue #{reference}")
+ expect(doc.text).to eq "Issue #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Issue #{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-issue attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-issue')
+ expect(link.attr('data-issue')).to eq issue.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Issue #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ 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
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ 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)
+
+ exp = act = "Issue #{reference}"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{invalidate_reference(reference)}"
+
+ 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]
+ end
+ end
+
+ context 'cross-project URL reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { helper.url_for_issue(issue.iid, project2) + "#note_123" }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq reference
+ end
+
+ it 'links with adjacent text' 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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'links with adjacent text' 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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
+ end
+
+ it 'links with adjacent text' 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
+end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
new file mode 100644
index 00000000000..b46ccc47605
--- /dev/null
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -0,0 +1,179 @@
+require 'spec_helper'
+require 'html/pipeline'
+
+describe Banzai::Filter::LabelReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:empty_project, :public) }
+ let(:label) { create(:label, project: project) }
+ let(:reference) { label.to_reference }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Label #{reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Label #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Label #{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-label attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-label')
+ expect(link.attr('data-label')).to eq label.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Label #{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_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}")
+ expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
+ end
+
+ it 'includes a style attribute' do
+ doc = reference_filter("Label #{reference}")
+ expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
+ end
+ end
+
+ context 'Integer-based references' do
+ 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)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label IDs' do
+ exp = act = "Label #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'String-based single-word references' 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.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}"
+
+ 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(: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.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
+
+ 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'
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'referencing a label in a link href' do
+ let(:reference) { %Q{<a href="#{label.to_reference}">Label</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_issues_url(project.namespace, project, label_name: label.name)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+>Label</a>\.\)))
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Label #{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-label attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ 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
+end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
new file mode 100644
index 00000000000..352710df307
--- /dev/null
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -0,0 +1,142 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project, :public) }
+ let(:merge) { create(:merge_request, source_project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { merge.to_reference }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_merge_request_url(project.namespace, project, merge)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid merge IDs' do
+ exp = act = "Merge #{invalidate_reference(reference)}"
+
+ 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}"
+ end
+
+ it 'escapes the title attribute' do
+ merge.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = reference_filter("Merge #{reference}")
+ expect(doc.text).to eq "Merge #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Merge #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Merge #{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-merge-request attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-merge-request')
+ expect(link.attr('data-merge-request')).to eq merge.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Merge #{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_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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:merge) { create(:merge_request, source_project: project2) }
+ let(:reference) { merge.to_reference(project) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_merge_request_url(project2.namespace,
+ project, merge)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid merge IDs on the referenced project' do
+ exp = act = "Merge #{invalidate_reference(reference)}"
+
+ 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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, :public, namespace: namespace) }
+ let(:merge) { create(:merge_request, source_project: project2, target_project: project2) }
+ let(:reference) { urls.namespace_project_merge_request_url(project2.namespace, project2, merge) + '/diffs#note_123' }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq reference
+ end
+
+ it 'links with adjacent text' 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/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb
new file mode 100644
index 00000000000..e9bb388e361
--- /dev/null
+++ b/spec/lib/banzai/filter/redactor_filter_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Banzai::Filter::RedactorFilter, lib: true do
+ include ActionView::Helpers::UrlHelper
+ include FilterSpecHelper
+
+ it 'ignores non-GFM links' do
+ html = %(See <a href="https://google.com/">Google</a>)
+ doc = filter(html, current_user: double)
+
+ expect(doc.css('a').length).to eq 1
+ end
+
+ def reference_link(data)
+ link_to('text', '', class: 'gfm', data: data)
+ end
+
+ context 'with data-project' do
+ it 'removes unpermitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+
+ link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 0
+ end
+
+ it 'allows permitted Project references' do
+ user = create(:user)
+ project = create(:empty_project)
+ project.team << [user, :master]
+
+ link = reference_link(project: project.id, reference_filter: 'ReferenceFilter')
+ 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')
+
+ expect { filter(link) }.not_to raise_error
+ 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')
+ doc = filter(link, current_user: user)
+
+ expect(doc.css('a').length).to eq 0
+ 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')
+ 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')
+
+ expect { filter(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')
+ doc = filter(link)
+
+ expect(doc.css('a').length).to eq 1
+ end
+ 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
new file mode 100644
index 00000000000..c8b1dfdf944
--- /dev/null
+++ b/spec/lib/banzai/filter/reference_gatherer_filter_spec.rb
@@ -0,0 +1,87 @@
+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
new file mode 100644
index 00000000000..0e6685f0ffb
--- /dev/null
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -0,0 +1,155 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Banzai::Filter::RelativeLinkFilter, lib: true do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
+ commit: project.commit,
+ project: project,
+ project_wiki: project_wiki,
+ ref: ref,
+ requested_path: requested_path
+ })
+
+ described_class.call(doc, contexts)
+ end
+
+ def image(path)
+ %(<img src="#{path}" />)
+ end
+
+ def link(path)
+ %(<a href="#{path}">#{path}</a>)
+ end
+
+ let(:project) { create(:project) }
+ let(:project_path) { project.path_with_namespace }
+ let(:ref) { 'markdown' }
+ let(:project_wiki) { nil }
+ let(:requested_path) { '/' }
+
+ shared_examples :preserve_unchanged do
+ it 'does not modify any relative URL in anchor' do
+ doc = filter(link('README.md'))
+ expect(doc.at_css('a')['href']).to eq 'README.md'
+ end
+
+ it 'does not modify any relative URL in image' do
+ doc = filter(image('files/images/logo-black.png'))
+ expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
+ end
+ end
+
+ shared_examples :relative_to_requested do
+ it 'rebuilds URL relative to the requested path' do
+ doc = filter(link('users.md'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/users.md"
+ end
+ end
+
+ context 'with a project_wiki' do
+ let(:project_wiki) { double('ProjectWiki') }
+ include_examples :preserve_unchanged
+ end
+
+ context 'without a repository' do
+ let(:project) { create(:empty_project) }
+ include_examples :preserve_unchanged
+ end
+
+ context 'with an empty repository' do
+ let(:project) { create(:project_empty_repo) }
+ include_examples :preserve_unchanged
+ end
+
+ it 'does not raise an exception on invalid URIs' do
+ act = link("://foo")
+ expect { filter(act) }.not_to raise_error
+ end
+
+ context 'with a valid repository' do
+ it 'rebuilds relative URL for a file in the repo' do
+ doc = filter(link('doc/api/README.md'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
+ it 'rebuilds relative URL for a file in the repo up one directory' do
+ relative_link = link('../api/README.md')
+ doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
+ it 'rebuilds relative URL for a file in the repo up multiple directories' do
+ relative_link = link('../../../api/README.md')
+ doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
+ it 'rebuilds relative URL for a file in the repository root' do
+ relative_link = link('../README.md')
+ doc = filter(relative_link, requested_path: 'doc/some-file.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/README.md"
+ end
+
+ it 'rebuilds relative URL for a file in the repo with an anchor' do
+ doc = filter(link('README.md#section'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/README.md#section"
+ end
+
+ it 'rebuilds relative URL for a directory in the repo' do
+ doc = filter(link('doc/api/'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/tree/#{ref}/doc/api"
+ end
+
+ it 'rebuilds relative URL for an image in the repo' do
+ doc = filter(link('files/images/logo-black.png'))
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
+ end
+
+ it 'does not modify relative URL with an anchor only' do
+ doc = filter(link('#section-1'))
+ expect(doc.at_css('a')['href']).to eq '#section-1'
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it 'supports Unicode filenames' 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)
+
+ doc = filter(image(escaped))
+ expect(doc.at_css('img')['src']).to match '/raw/'
+ end
+
+ context 'when requested path is a file in the repo' do
+ let(:requested_path) { 'doc/api/README.md' }
+ include_examples :relative_to_requested
+ end
+
+ context 'when requested path is a directory in the repo' do
+ let(:requested_path) { 'doc/api' }
+ include_examples :relative_to_requested
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
new file mode 100644
index 00000000000..760d60a4190
--- /dev/null
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -0,0 +1,197 @@
+require 'spec_helper'
+
+describe Banzai::Filter::SanitizationFilter, lib: true do
+ include FilterSpecHelper
+
+ describe 'default whitelist' do
+ it 'sanitizes tags that are not whitelisted' do
+ act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
+ exp = 'no inputs and no blinks'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'sanitizes tag attributes' do
+ act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
+ exp = %q{<a href="http://example.com/bar.html">Text</a>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'sanitizes javascript in attributes' do
+ act = %q(<a href="javascript:alert('foo')">Text</a>)
+ exp = '<a>Text</a>'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'allows whitelisted HTML tags from the user' do
+ exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'sanitizes `class` attribute on any element' do
+ act = %q{<strong class="foo">Strong</strong>}
+ expect(filter(act).to_html).to eq %q{<strong>Strong</strong>}
+ end
+
+ it 'sanitizes `id` attribute on any element' do
+ act = %q{<em id="foo">Emphasis</em>}
+ expect(filter(act).to_html).to eq %q{<em>Emphasis</em>}
+ end
+ end
+
+ describe 'custom whitelist' do
+ it 'customizes the whitelist only once' do
+ instance = described_class.new('Foo')
+ 3.times { instance.whitelist }
+
+ expect(instance.whitelist[:transformers].size).to eq 5
+ end
+
+ it 'allows syntax highlighting' do
+ exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'sanitizes `class` attribute from non-highlight spans' do
+ act = %q{<span class="k">def</span>}
+ expect(filter(act).to_html).to eq %q{<span>def</span>}
+ end
+
+ it 'allows `style` attribute on table elements' do
+ html = <<-HTML.strip_heredoc
+ <table>
+ <tr><th style="text-align: center">Head</th></tr>
+ <tr><td style="text-align: right">Body</th></tr>
+ </table>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('th')['style']).to eq 'text-align: center'
+ expect(doc.at_css('td')['style']).to eq 'text-align: right'
+ end
+
+ it 'allows `span` elements' do
+ exp = act = %q{<span>Hello</span>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'removes `rel` attribute from `a` elements' do
+ act = %q{<a href="#" rel="nofollow">Link</a>}
+ exp = %q{<a href="#">Link</a>}
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ # Adapted from the Sanitize test suite: http://git.io/vczrM
+ protocols = {
+ 'protocol-based JS injection: simple, no spaces' => {
+ input: '<a href="javascript:alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: simple, spaces before' => {
+ input: '<a href="javascript :alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: simple, spaces after' => {
+ input: '<a href="javascript: alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: simple, spaces before and after' => {
+ input: '<a href="javascript : alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: preceding colon' => {
+ input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: UTF-8 encoding' => {
+ input: '<a href="javascript&#58;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: long UTF-8 encoding' => {
+ input: '<a href="javascript&#0058;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: long UTF-8 encoding without semicolons' => {
+ input: '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: hex encoding' => {
+ input: '<a href="javascript&#x3A;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: long hex encoding' => {
+ input: '<a href="javascript&#x003A;">foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: hex encoding without semicolons' => {
+ input: '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
+ output: '<a>foo</a>'
+ },
+
+ 'protocol-based JS injection: null char' => {
+ input: "<a href=java\0script:alert(\"XSS\")>foo</a>",
+ output: '<a href="java"></a>'
+ },
+
+ 'protocol-based JS injection: spaces and entities' => {
+ input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
+ output: '<a href="">foo</a>'
+ },
+ }
+
+ protocols.each do |name, data|
+ it "handles #{name}" do
+ doc = filter(data[:input])
+
+ expect(doc.to_html).to eq data[:output]
+ end
+ end
+
+ it 'allows non-standard anchor schemes' do
+ exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>}
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
+
+ it 'allows relative links' do
+ exp = %q{<a href="foo/bar.md">foo/bar.md</a>}
+ act = filter(exp)
+
+ expect(act.to_html).to eq exp
+ end
+ end
+
+ context 'when inline_sanitization is true' do
+ it 'uses a stricter whitelist' do
+ doc = filter('<h1>Description</h1>', inline_sanitization: true)
+ expect(doc.to_html.strip).to eq 'Description'
+ end
+
+ %w(pre code img ol ul li).each do |elem|
+ it "removes '#{elem}' elements" do
+ act = "<#{elem}>Description</#{elem}>"
+ expect(filter(act, inline_sanitization: true).to_html.strip).
+ to eq 'Description'
+ end
+ end
+
+ %w(b i strong em a ins del sup sub p).each do |elem|
+ it "still allows '#{elem}' elements" do
+ exp = act = "<#{elem}>Description</#{elem}>"
+ expect(filter(act, inline_sanitization: true).to_html).to eq exp
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
new file mode 100644
index 00000000000..26466fbb180
--- /dev/null
+++ b/spec/lib/banzai/filter/snippet_reference_filter_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe Banzai::Filter::SnippetReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:empty_project, :public) }
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:reference) { snippet.to_reference }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_snippet_url(project.namespace, project, snippet)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Snippet (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs' do
+ exp = act = "Snippet #{invalidate_reference(reference)}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("Snippet #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
+ end
+
+ it 'escapes the title attribute' do
+ snippet.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = reference_filter("Snippet #{reference}")
+ expect(doc.text).to eq "Snippet #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Snippet #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("Snippet #{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-snippet attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-snippet')
+ expect(link.attr('data-snippet')).to eq snippet.id.to_s
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Snippet #{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_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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { snippet.to_reference(project) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs on the referenced project' do
+ exp = act = "See #{invalidate_reference(reference)}"
+
+ 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
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, :public, namespace: namespace) }
+ let(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { urls.namespace_project_snippet_url(project2.namespace, project2, snippet) }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(snippet.to_reference(project))}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs on the referenced project' do
+ act = "See #{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("Snippet #{reference}")
+ expect(result[:references][:snippet]).to eq [snippet]
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
new file mode 100644
index 00000000000..407617f3307
--- /dev/null
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'highlights valid code blocks' do
+ result = filter('<pre><code>def fun end</code>')
+ expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
+ end
+
+ it 'passes through invalid code blocks' do
+ allow_any_instance_of(described_class).to receive(:block_code).and_raise(StandardError)
+
+ result = filter('<pre><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre>This is a test</pre>')
+ end
+end
diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
new file mode 100644
index 00000000000..6a5d003e87f
--- /dev/null
+++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb
@@ -0,0 +1,97 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Banzai::Filter::TableOfContentsFilter, lib: true do
+ include FilterSpecHelper
+
+ def header(level, text)
+ "<h#{level}>#{text}</h#{level}>\n"
+ end
+
+ it 'does nothing when :no_header_anchors is truthy' do
+ exp = act = header(1, 'Header')
+ expect(filter(act, no_header_anchors: 1).to_html).to eq exp
+ end
+
+ it 'does nothing with empty headers' do
+ exp = act = header(1, nil)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ 1.upto(6) do |i|
+ it "processes h#{i} elements" do
+ html = header(i, "Header #{i}")
+ doc = filter(html)
+
+ expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
+ end
+ end
+
+ describe 'anchor tag' do
+ it 'has an `anchor` class' do
+ doc = filter(header(1, 'Header'))
+ expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
+ end
+
+ it 'links to the id' do
+ doc = filter(header(1, 'Header'))
+ expect(doc.css('h1 a').first.attr('href')).to eq '#header'
+ end
+
+ describe 'generated IDs' do
+ it 'translates spaces to dashes' do
+ doc = filter(header(1, 'This header has spaces in it'))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
+ end
+
+ it 'squeezes multiple spaces and dashes' do
+ doc = filter(header(1, 'This---header is poorly-formatted'))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
+ end
+
+ it 'removes punctuation' do
+ doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
+ end
+
+ it 'appends a unique number to duplicates' do
+ doc = filter(header(1, 'One') + header(2, 'One'))
+
+ expect(doc.css('h1 a').first.attr('id')).to eq 'one'
+ expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
+ end
+
+ it 'supports Unicode' do
+ doc = filter(header(1, '한글'))
+ expect(doc.css('h1 a').first.attr('id')).to eq '한글'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
+ end
+ end
+ end
+
+ describe 'result' do
+ def result(html)
+ HTML::Pipeline.new([described_class]).call(html)
+ end
+
+ let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) }
+ let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
+
+ it 'is contained within a `ul` element' do
+ expect(doc.children.first.name).to eq 'ul'
+ expect(doc.children.first.attr('class')).to eq 'section-nav'
+ end
+
+ it 'contains an `li` element for each header' do
+ expect(doc.css('li').length).to eq 2
+
+ links = doc.css('li a')
+
+ expect(links.first.attr('href')).to eq '#header-1'
+ expect(links.first.text).to eq 'Header 1'
+ expect(links.last.attr('href')).to eq '#header-2'
+ expect(links.last.text).to eq 'Header 2'
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
new file mode 100644
index 00000000000..f2e3a44478d
--- /dev/null
+++ b/spec/lib/banzai/filter/task_list_filter_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe Banzai::Filter::TaskListFilter, lib: true do
+ include FilterSpecHelper
+
+ it 'does not apply `task-list` class to non-task lists' do
+ exp = act = %(<ul><li>Item</li></ul>)
+ expect(filter(act).to_html).to eq exp
+ end
+end
diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb
new file mode 100644
index 00000000000..3b073a90a95
--- /dev/null
+++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb
@@ -0,0 +1,73 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+describe Banzai::Filter::UploadLinkFilter, lib: true do
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
+ project: project
+ })
+
+ described_class.call(doc, contexts)
+ end
+
+ def image(path)
+ %(<img src="#{path}" />)
+ end
+
+ def link(path)
+ %(<a href="#{path}">#{path}</a>)
+ end
+
+ let(:project) { create(:project) }
+
+ shared_examples :preserve_unchanged do
+ it 'does not modify any relative URL in anchor' do
+ doc = filter(link('README.md'))
+ expect(doc.at_css('a')['href']).to eq 'README.md'
+ end
+
+ it 'does not modify any relative URL in image' do
+ doc = filter(image('files/images/logo-black.png'))
+ expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
+ end
+ end
+
+ it 'does not raise an exception on invalid URIs' do
+ act = link("://foo")
+ expect { filter(act) }.not_to raise_error
+ end
+
+ context 'with a valid repository' do
+ it 'rebuilds relative URL for a link' 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"
+ end
+
+ it 'rebuilds relative URL for an image' 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"
+ end
+
+ it 'does not modify absolute URL' do
+ doc = filter(link('http://example.com'))
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it 'supports Unicode filenames' do
+ path = '/uploads/한글.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)
+
+ doc = filter(image(escaped))
+ expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png"
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
new file mode 100644
index 00000000000..8bdebae1841
--- /dev/null
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -0,0 +1,160 @@
+require 'spec_helper'
+
+describe Banzai::Filter::UserReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+ let(:reference) { user.to_reference }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ it 'ignores invalid users' do
+ exp = act = "Hey #{invalidate_reference(reference)}"
+ expect(reference_filter(act).to_html).to eq(exp)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Hey #{reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'mentioning @all' do
+ let(:reference) { User.reference_prefix + 'all' }
+
+ before do
+ project.team << [project.creator, :developer]
+ end
+
+ it 'supports a special @all mention' do
+ doc = reference_filter("Hey #{reference}")
+ 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 'adds to the results hash' do
+ result = reference_pipeline_result("Hey #{reference}", author: project.creator)
+ expect(result[:references][:user]).to eq [project.creator]
+ end
+ end
+
+ context "when the author is not a member of the project" do
+
+ let(:other_user) { create(:user) }
+
+ 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
+ end
+ end
+
+ context 'mentioning a user' do
+ it 'links to a User' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ it 'links to a User with a period' do
+ user = create(:user, name: 'alphA.Beta')
+
+ doc = reference_filter("Hey #{user.to_reference}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'links to a User with an underscore' do
+ user = create(:user, name: 'ping_pong_king')
+
+ doc = reference_filter("Hey #{user.to_reference}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'includes a data-user attribute' do
+ doc = reference_filter("Hey #{reference}")
+ link = doc.css('a').first
+
+ 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
+ let(:group) { create(:group) }
+ let(:reference) { group.to_reference }
+
+ it 'links to the Group' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'includes a data-group attribute' do
+ doc = reference_filter("Hey #{reference}")
+ link = doc.css('a').first
+
+ 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
+ doc = reference_filter("Mention me (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+ end
+
+ it 'supports an :only_path context' do
+ doc = reference_filter("Hey #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.user_path(user)
+ end
+
+ context 'referencing a user in a link href' do
+ let(:reference) { %Q{<a href="#{user.to_reference}">User</a>} }
+
+ it 'links to a User' do
+ doc = reference_filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("Mention me (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>User<\/a>\.\)/)
+ end
+
+ it 'includes a data-user attribute' do
+ doc = reference_filter("Hey #{reference}")
+ link = doc.css('a').first
+
+ 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
+end
diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb
new file mode 100644
index 00000000000..27da2a7439c
--- /dev/null
+++ b/spec/lib/banzai/querying_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Banzai::Querying do
+ describe '.css' do
+ it 'optimizes queries for elements with classes' do
+ document = double(:document)
+
+ expect(document).to receive(:xpath).with(/^descendant::a/)
+
+ described_class.css(document, 'a.gfm')
+ end
+ end
+end
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
index 75c023bbc43..3a2b568f4c7 100644
--- a/spec/lib/ci/ansi2html_spec.rb
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Ci::Ansi2html do
+describe Ci::Ansi2html, lib: true do
subject { Ci::Ansi2html }
it "prints non-ansi as-is" do
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
index 83e2ad220b8..50a77308cde 100644
--- a/spec/lib/ci/charts_spec.rb
+++ b/spec/lib/ci/charts_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Charts" do
+describe Ci::Charts, lib: true do
context "build_times" do
before do
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 2260a6f8130..d15100fc6d8 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
module Ci
- describe GitlabCiYamlProcessor do
+ describe GitlabCiYamlProcessor, lib: true do
+ let(:path) { 'path' }
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -12,7 +13,7 @@ module Ci
rspec: { script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
@@ -29,77 +30,217 @@ module Ci
})
end
- it "does not return builds if only has another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["deploy"] }
- })
+ describe :only do
+ it "does not return builds if only has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["deploy"] }
+ })
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
- it "does not return builds if only has regexp with another branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["/^deploy$/"] }
- })
+ it "does not return builds if only has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["/^deploy$/"] }
+ })
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
- end
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
- it "returns builds if only has specified this branch" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", only: ["master"] }
- })
+ it "returns builds if only has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["master"] }
+ })
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
- end
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
- it "does not build tags" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", except: ["tags"] }
- })
+ it "returns builds if only has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
+ })
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- expect(config_processor.builds_for_stage_and_ref(type, "0-1", true).size).to eq(0)
- end
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
- it "returns builds if only has a list of branches including specified" do
- config = YAML.dump({
- before_script: ["pwd"],
- rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
- })
+ it "returns builds if only has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches"] }
+ })
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if only has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["tags"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if only has current repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@path"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if only has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["branches@fork"] }
+ })
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns build only for specified type" do
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", only: ["master", "deploy"] },
+ staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] },
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, 'fork')
+
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ 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
end
- it "returns build only for specified type" do
+ describe :except do
+ it "returns builds if except has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["deploy"] }
+ })
- config = YAML.dump({
- before_script: ["pwd"],
- build: { script: "build", type: "build", only: ["master", "deploy"] },
- rspec: { script: "rspec", type: type, only: ["master", "deploy"] },
- staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
- production: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
- })
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "returns builds if except has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["/^deploy$/"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "does not return builds if except has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["master"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "does not return builds if except has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["master", "deploy"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "does not return builds if except has a branches keyword specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if except has a tags keyword" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["tags"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "does not return builds if except has current repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@path"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
+ end
+
+ it "returns builds if except has different repository path" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, except: ["branches@fork"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, path)
- expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0)
- expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
- expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "returns build except specified type" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
+ staging: { script: "deploy", type: "deploy", except: ["master"] },
+ production: { script: "deploy", type: "deploy", except: ["master@fork"] },
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config, 'fork')
+
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ 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
+
end
describe "Image and service handling" do
@@ -111,7 +252,7 @@ module Ci
rspec: { script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
@@ -139,7 +280,7 @@ module Ci
rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
@@ -172,7 +313,7 @@ module Ci
rspec: { script: "rspec" }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.variables).to eq(variables)
end
end
@@ -184,7 +325,7 @@ module Ci
rspec: { script: "rspec", when: when_state }
})
- config_processor = GitlabCiYamlProcessor.new(config)
+ 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)
@@ -192,150 +333,301 @@ module Ci
end
end
+ describe "Caches" do
+ it "returns cache when defined globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true },
+ rspec: {
+ script: "rspec"
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ )
+ end
+
+ it "returns cache when defined in a job" do
+ config = YAML.dump({
+ rspec: {
+ cache: { paths: ["logs/", "binaries/"], untracked: true },
+ script: "rspec"
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["logs/", "binaries/"],
+ untracked: true,
+ )
+ end
+
+ it "overwrite cache when defined for a job and globally" do
+ config = YAML.dump({
+ cache: { paths: ["logs/", "binaries/"], untracked: true },
+ rspec: {
+ script: "rspec",
+ cache: { paths: ["test/"], untracked: false },
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
+ paths: ["test/"],
+ untracked: false,
+ )
+ end
+ end
+
+ describe "Artifacts" do
+ it "returns artifacts when defined" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: {
+ artifacts: { paths: ["logs/", "binaries/"], untracked: true },
+ script: "rspec"
+ }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :rspec,
+ only: nil,
+ commands: "pwd\nrspec",
+ tag_list: [],
+ options: {
+ image: "ruby:2.1",
+ services: ["mysql"],
+ artifacts: {
+ paths: ["logs/", "binaries/"],
+ untracked: true
+ }
+ },
+ when: "on_success",
+ allow_failure: false
+ })
+ end
+ end
+
describe "Error handling" do
+ it "fails to parse YAML" do
+ expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
+ end
+
it "indicates that object is invalid" do
- expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
+ expect{GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
end
it "returns errors if tags parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
end
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string")
end
+ it "returns errors if job name is blank" do
+ config = YAML.dump({ '' => { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end
+
+ it "returns errors if job name is non-string" do
+ config = YAML.dump({ 10 => { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string")
+ end
+
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string")
end
it "returns errors if services parameter is not an array" do
config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services 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)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
end
it "returns errors if job services parameter is not an array" do
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
end
it "returns errors if there are unknown parameters" do
config = YAML.dump({ extra: "bundle update" })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
end
it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
config = YAML.dump({ extra: { services: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
end
- it "returns errors if there is no any jobs defined" do
+ it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean")
end
it "returns errors if job stage is not a string" do
- config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } })
+ config = YAML.dump({ rspec: { script: "test", type: 1 } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a pre-defined stage" do
- config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
+ config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a defined stage" do
- config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
end
it "returns errors if stages is not an array" do
config = YAML.dump({ types: "test", rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages 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" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages 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)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end
it "returns errors if variables is not a map of key-valued strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
end
it "returns errors if job when is not on_success, on_failure or always" do
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
- GitlabCiYamlProcessor.new(config)
+ GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always")
end
+
+ it "returns errors if job artifacts:untracked is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean")
+ end
+
+ it "returns errors if job artifacts:paths is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings")
+ end
+
+ it "returns errors if cache:untracked is not an array of strings" do
+ 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
+
+ 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
+
+ it "returns errors if job cache:untracked is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean")
+ end
+
+ it "returns errors if job cache:paths is not an array of strings" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings")
+ end
end
end
end
diff --git a/spec/lib/disable_email_interceptor_spec.rb b/spec/lib/disable_email_interceptor_spec.rb
index a9624e9a2b7..c2a7b20b84d 100644
--- a/spec/lib/disable_email_interceptor_spec.rb
+++ b/spec/lib/disable_email_interceptor_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe DisableEmailInterceptor do
+describe DisableEmailInterceptor, lib: true do
before do
ActionMailer::Base.register_interceptor(DisableEmailInterceptor)
end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 48bc60eed16..f38fadda9ba 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe ExtractsPath do
+describe ExtractsPath, lib: true do
include ExtractsPath
include RepoHelpers
include Gitlab::Application.routes.url_helpers
diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb
index 12ccc051c74..fda6f9a6c88 100644
--- a/spec/lib/file_size_validator_spec.rb
+++ b/spec/lib/file_size_validator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Gitlab::FileSizeValidatorSpec' do
+describe FileSizeValidator, lib: true do
let(:validator) { FileSizeValidator.new(options) }
let(:attachment) { AttachmentUploader.new }
let(:note) { create(:note) }
diff --git a/spec/lib/git_ref_validator_spec.rb b/spec/lib/git_ref_validator_spec.rb
index 4633b6f3934..dc57e94f193 100644
--- a/spec/lib/git_ref_validator_spec.rb
+++ b/spec/lib/git_ref_validator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GitRefValidator do
+describe Gitlab::GitRefValidator, lib: true do
it { expect(Gitlab::GitRefValidator.validate('feature/new')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('implement_@all')).to be_truthy }
it { expect(Gitlab::GitRefValidator.validate('my_new_feature')).to be_truthy }
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 03e36fd3552..6beb21c6d2b 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
require 'nokogiri'
module Gitlab
- describe Asciidoc do
+ describe Asciidoc, lib: true do
let(:input) { '<b>ascii</b>' }
let(:context) { {} }
@@ -50,9 +50,9 @@ module Gitlab
filtered_html = '<b>ASCII</b>'
allow(Asciidoctor).to receive(:convert).and_return(html)
- expect_any_instance_of(HTML::Pipeline).to receive(:call)
- .with(html, context)
- .and_return(output: Nokogiri::HTML.fragment(filtered_html))
+ expect(Banzai).to receive(:render)
+ .with(html, context.merge(pipeline: :asciidoc))
+ .and_return(filtered_html)
expect( render('foo', context) ).to eql filtered_html
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 72806bebe1f..aad291c03cd 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Auth do
+describe Gitlab::Auth, lib: true do
let(:gl_auth) { Gitlab::Auth.new }
describe :find do
diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
index 37c527221a0..cd26dca0998 100644
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ b/spec/lib/gitlab/backend/grack_auth_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Grack::Auth do
+describe Grack::Auth, lib: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -50,6 +50,22 @@ describe Grack::Auth do
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"
@@ -175,15 +191,10 @@ describe Grack::Auth do
context "when a gitlab ci token is provided" do
let(:token) { "123" }
- let(:gitlab_ci_project) { FactoryGirl.create :ci_project, token: token }
+ let(:project) { FactoryGirl.create :empty_project }
before do
- project.gitlab_ci_project = gitlab_ci_project
- project.save
-
- gitlab_ci_service = project.build_gitlab_ci_service
- gitlab_ci_service.active = true
- gitlab_ci_service.save
+ project.update_attributes(runners_token: token, builds_enabled: true)
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token)
end
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index b60e23454d6..fd869f48b5c 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Shell do
+describe Gitlab::Shell, lib: true do
let(:project) { double('Project', id: 7, path: 'diaspora') }
let(:gitlab_shell) { Gitlab::Shell.new }
@@ -16,7 +16,7 @@ describe Gitlab::Shell do
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
- describe Gitlab::Shell::KeyAdder do
+ describe Gitlab::Shell::KeyAdder, lib: true do
describe '#add_key' do
it 'normalizes space characters in the key' do
io = spy
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
index dfe58637eee..aa0699f2ebf 100644
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::BitbucketImport::Client do
+describe Gitlab::BitbucketImport::Client, lib: true do
let(:token) { '123456' }
let(:secret) { 'secret' }
let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index 0e826a319e0..e1c60e07b4d 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::BitbucketImport::ProjectCreator do
+describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
let(:repo) do
{
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb
new file mode 100644
index 00000000000..839b30f1ff4
--- /dev/null
+++ b/spec/lib/gitlab/build_data_builder_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe 'Gitlab::BuildDataBuilder' do
+ let(:build) { create(:ci_build) }
+
+ describe :build do
+ let(:data) do
+ Gitlab::BuildDataBuilder.build(build)
+ end
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:ref]).to eq(build.ref) }
+ it { expect(data[:sha]).to eq(build.sha) }
+ it { expect(data[:tag]).to eq(build.tag) }
+ it { expect(data[:build_id]).to eq(build.id) }
+ it { expect(data[:build_status]).to eq(build.status) }
+ it { expect(data[:project_id]).to eq(build.project.id) }
+ it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
+ end
+end
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index 21254f778d3..99288da1e43 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -1,12 +1,19 @@
require 'spec_helper'
-describe Gitlab::ClosingIssueExtractor do
+describe Gitlab::ClosingIssueExtractor, lib: true do
let(:project) { create(:project) }
+ let(:project2) { create(:project) }
let(:issue) { create(:issue, project: project) }
+ let(:issue2) { create(:issue, project: project2) }
let(:reference) { issue.to_reference }
+ let(:cross_reference) { issue2.to_reference(project) }
subject { described_class.new(project, project.creator) }
+ before do
+ project2.team << [project.creator, :master]
+ end
+
describe "#closed_by_message" do
context 'with a single reference' do
it do
@@ -130,6 +137,27 @@ describe Gitlab::ClosingIssueExtractor do
end
end
+ context "with a cross-project reference" do
+ it do
+ message = "Closes #{cross_reference}"
+ expect(subject.closed_by_message(message)).to eq([issue2])
+ end
+ end
+
+ context "with a cross-project URL" do
+ it do
+ message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)}"
+ expect(subject.closed_by_message(message)).to eq([issue2])
+ end
+ end
+
+ context "with an invalid URL" do
+ it do
+ message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)}"
+ expect(subject.closed_by_message(message)).to eq([])
+ end
+ end
+
context 'with multiple references' do
let(:other_issue) { create(:issue, project: project) }
let(:third_issue) { create(:issue, project: project) }
@@ -171,6 +199,31 @@ describe Gitlab::ClosingIssueExtractor do
expect(subject.closed_by_message(message)).
to match_array([issue, other_issue, third_issue])
end
+
+ it "fetches cross-project references" do
+ message = "Closes #{reference} and #{cross_reference}"
+
+ expect(subject.closed_by_message(message)).
+ to match_array([issue, issue2])
+ end
+
+ it "fetches cross-project URL references" do
+ message = "Closes #{urls.namespace_project_issue_url(issue2.project.namespace, issue2.project, issue2)} and #{reference}"
+
+ expect(subject.closed_by_message(message)).
+ to match_array([issue, issue2])
+ end
+
+ it "ignores invalid cross-project URL references" do
+ message = "Closes https://google.com#{urls.namespace_project_issue_path(issue2.project.namespace, issue2.project, issue2)} and #{reference}"
+
+ expect(subject.closed_by_message(message)).
+ to match_array([issue])
+ end
end
end
+
+ def urls
+ Gitlab::Application.routes.url_helpers
+ end
end
diff --git a/spec/lib/gitlab/color_schemes_spec.rb b/spec/lib/gitlab/color_schemes_spec.rb
index c7be45dbcd3..0a1ec66f199 100644
--- a/spec/lib/gitlab/color_schemes_spec.rb
+++ b/spec/lib/gitlab/color_schemes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::ColorSchemes do
+describe Gitlab::ColorSchemes, lib: true do
describe '.body_classes' do
it 'returns a space-separated list of class names' do
css = described_class.body_classes
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 7cdebdf209a..8461e8ce50d 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Database do
+describe Gitlab::Database, lib: true do
# These are just simple smoke tests to check if the methods work (regardless
# of what they may return).
describe '.mysql?' do
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 8b7946f3117..c7cdf8691d6 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Diff::File do
+describe Gitlab::Diff::File, lib: true do
include RepoHelpers
let(:project) { create(:project) }
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index 4d5d1431683..ba577bd28e5 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Diff::Parser do
+describe Gitlab::Diff::Parser, lib: true do
include RepoHelpers
let(:project) { create(:project) }
diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb
index 8fb432367b6..476a21bf996 100644
--- a/spec/lib/gitlab/email/attachment_uploader_spec.rb
+++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Gitlab::Email::AttachmentUploader do
+describe Gitlab::Email::AttachmentUploader, lib: true do
describe "#execute" do
let(:project) { build(:project) }
let(:message_raw) { fixture_file("emails/attachment.eml") }
diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb
new file mode 100644
index 00000000000..56ae2a8d121
--- /dev/null
+++ b/spec/lib/gitlab/email/message/repository_push_spec.rb
@@ -0,0 +1,122 @@
+require 'spec_helper'
+
+describe Gitlab::Email::Message::RepositoryPush do
+ include RepoHelpers
+
+ let!(:group) { create(:group, name: 'my_group') }
+ let!(:project) { create(:project, name: 'my_project', namespace: group) }
+ let!(:author) { create(:author, name: 'Author') }
+
+ let(:message) do
+ described_class.new(Notify, project.id, 'recipient@example.com', opts)
+ end
+
+ context 'new commits have been pushed to repository' do
+ let(:opts) do
+ { author_id: author.id, ref: 'master', action: :push, compare: compare,
+ send_from_committer_email: true }
+ end
+ let(:compare) do
+ Gitlab::Git::Compare.new(project.repository.raw_repository,
+ sample_image_commit.id, sample_commit.id)
+ end
+
+ describe '#project' do
+ subject { message.project }
+ it { is_expected.to eq project }
+ it { is_expected.to be_an_instance_of Project }
+ end
+
+ describe '#project_namespace' do
+ subject { message.project_namespace }
+ it { is_expected.to eq group }
+ it { is_expected.to be_kind_of Namespace }
+ end
+
+ describe '#project_name_with_namespace' do
+ subject { message.project_name_with_namespace }
+ it { is_expected.to eq 'my_group / my_project' }
+ end
+
+ describe '#author' do
+ subject { message.author }
+ it { is_expected.to eq author }
+ it { is_expected.to be_an_instance_of User }
+ end
+
+ describe '#author_name' do
+ subject { message.author_name }
+ it { is_expected.to eq 'Author' }
+ end
+
+ describe '#commits' do
+ subject { message.commits }
+ it { is_expected.to be_kind_of Array }
+ it { is_expected.to all(be_instance_of Commit) }
+ end
+
+ describe '#diffs' do
+ subject { message.diffs }
+ it { is_expected.to all(be_an_instance_of Gitlab::Git::Diff) }
+ end
+
+ describe '#diffs_count' do
+ subject { message.diffs_count }
+ it { is_expected.to eq compare.diffs.count }
+ end
+
+ describe '#compare' do
+ subject { message.compare }
+ it { is_expected.to be_an_instance_of Gitlab::Git::Compare }
+ end
+
+ describe '#compare_timeout' do
+ subject { message.compare_timeout }
+ it { is_expected.to eq compare.timeout }
+ end
+
+ describe '#reverse_compare?' do
+ subject { message.reverse_compare? }
+ it { is_expected.to eq false }
+ end
+
+ describe '#disable_diffs?' do
+ subject { message.disable_diffs? }
+ it { is_expected.to eq false }
+ end
+
+ describe '#send_from_committer_email?' do
+ subject { message.send_from_committer_email? }
+ it { is_expected.to eq true }
+ end
+
+ describe '#action_name' do
+ subject { message.action_name }
+ it { is_expected.to eq 'pushed to' }
+ end
+
+ describe '#ref_name' do
+ subject { message.ref_name }
+ it { is_expected.to eq 'master' }
+ end
+
+ describe '#ref_type' do
+ subject { message.ref_type }
+ it { is_expected.to eq 'branch' }
+ end
+
+ describe '#target_url' do
+ subject { message.target_url }
+ it { is_expected.to include 'compare' }
+ it { is_expected.to include compare.commits.first.parents.first.id }
+ it { is_expected.to include compare.commits.last.id }
+ end
+
+ describe '#subject' do
+ subject { message.subject }
+ it { is_expected.to include "[Git][#{project.path_with_namespace}]" }
+ it { is_expected.to include "#{compare.commits.length} commits" }
+ it { is_expected.to include compare.commits.first.message.split("\n").first }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index e470b7cd5f5..b535413bbd4 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Gitlab::Email::Receiver do
+describe Gitlab::Email::Receiver, lib: true do
before do
stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
end
diff --git a/spec/lib/gitlab/email/reply_parser_spec.rb b/spec/lib/gitlab/email/reply_parser_spec.rb
index 7cae1da8050..6f8e9a4be64 100644
--- a/spec/lib/gitlab/email/reply_parser_spec.rb
+++ b/spec/lib/gitlab/email/reply_parser_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
# Inspired in great part by Discourse's Email::Receiver
-describe Gitlab::Email::ReplyParser do
+describe Gitlab::Email::ReplyParser, lib: true do
describe '#execute' do
def test_parse_body(mail_string)
described_class.new(Mail::Message.new(mail_string)).execute
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index c7291689e32..9b3a0e3a75f 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GitAccess do
+describe Gitlab::GitAccess, lib: true do
let(:access) { Gitlab::GitAccess.new(actor, project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 4cb91094cb3..77ecfce6f17 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GitAccessWiki do
+describe Gitlab::GitAccessWiki, lib: true do
let(:access) { Gitlab::GitAccessWiki.new(user, project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 26618120316..49d8cdf4314 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::Client do
+describe Gitlab::GithubImport::Client, lib: true do
let(:token) { '123456' }
let(:client) { Gitlab::GithubImport::Client.new(token) }
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
new file mode 100644
index 00000000000..a324a82e69f
--- /dev/null
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::CommentFormatter, lib: true do
+ let(:project) { create(:project) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
+ let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
+ let(:base_data) do
+ {
+ body: "I'm having a problem with this.",
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ end
+
+ subject(:comment) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when do not reference a portion of the diff' do
+ let(:raw_data) { OpenStruct.new(base_data) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nI'm having a problem with this.",
+ commit_id: nil,
+ line_code: nil,
+ author_id: project.creator_id,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when on a portion of the diff' do
+ let(:diff_data) do
+ {
+ body: 'Great stuff',
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...',
+ path: 'file1.txt',
+ position: 1
+ }
+ end
+
+ let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nGreat stuff",
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1',
+ author_id: project.creator_id,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(comment.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
new file mode 100644
index 00000000000..fd05428b322
--- /dev/null
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -0,0 +1,139 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::IssueFormatter, lib: true do
+ let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+
+ let(:base_data) do
+ {
+ number: 1347,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil
+ }
+ end
+
+ subject(:issue) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when issue is open' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'opened',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when issue is closed' do
+ let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'closed',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: closed_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:assignee_id)).to be_nil
+ end
+
+ it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+
+ describe '#has_comments?' do
+ context 'when number of comments is greater than zero' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) }
+
+ it 'returns true' do
+ expect(issue.has_comments?).to eq true
+ end
+ end
+
+ context 'when number of comments is equal to zero' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) }
+
+ it 'returns false' do
+ expect(issue.has_comments?).to eq false
+ end
+ end
+ end
+
+ describe '#number' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+
+ it 'returns pull request number' do
+ expect(issue.number).to eq 1347
+ end
+ end
+
+ describe '#valid?' do
+ context 'when mention a pull request' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) }
+
+ it 'returns false' do
+ expect(issue.valid?).to eq false
+ end
+ end
+
+ context 'when does not mention a pull request' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) }
+
+ it 'returns true' do
+ expect(issue.valid?).to eq true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index ca61d3c5234..c93a3ebdaec 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GithubImport::ProjectCreator do
+describe Gitlab::GithubImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
let(:repo) do
OpenStruct.new(
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
new file mode 100644
index 00000000000..9aefec77f6d
--- /dev/null
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -0,0 +1,184 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
+ let(:project) { create(:project) }
+ let(:source_branch) { OpenStruct.new(ref: 'feature') }
+ let(:target_branch) { OpenStruct.new(ref: 'master') }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:base_data) do
+ {
+ number: 1347,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil
+ }
+ end
+
+ subject(:pull_request) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when pull request is open' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ state: 'opened',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when pull request is closed' do
+ let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ state: 'closed',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: closed_at
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when pull request is merged' do
+ let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ title: 'New feature',
+ description: "*Created by: octocat*\n\nPlease pull these awesome changes",
+ source_project: project,
+ source_branch: 'feature',
+ target_project: project,
+ target_branch: 'master',
+ state: 'merged',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: merged_at
+ }
+
+ expect(pull_request.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(pull_request.attributes.fetch(:assignee_id)).to be_nil
+ end
+
+ it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+
+ describe '#cross_project?' do
+ context 'when source repo is not a fork' do
+ let(:local_repo) { OpenStruct.new(fork: false) }
+ let(:source_branch) { OpenStruct.new(ref: 'feature', repo: local_repo) }
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.cross_project?).to eq false
+ end
+ end
+
+ context 'when source repo is a fork' do
+ let(:forked_repo) { OpenStruct.new(fork: true) }
+ let(:source_branch) { OpenStruct.new(ref: 'feature', repo: forked_repo) }
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch)) }
+
+ it 'returns true' do
+ expect(pull_request.cross_project?).to eq true
+ end
+ end
+ end
+
+ describe '#number' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+
+ it 'returns pull request number' do
+ expect(pull_request.number).to eq 1347
+ end
+ end
+
+ describe '#valid?' do
+ let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') }
+
+ context 'when source and target branches exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) }
+
+ it 'returns true' do
+ expect(pull_request.valid?).to eq true
+ end
+ end
+
+ context 'when source branch doesn not exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+
+ context 'when target branch doesn not exists' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) }
+
+ it 'returns false' do
+ expect(pull_request.valid?).to eq false
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitlab_import/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb
index c511c515474..e6831e7c383 100644
--- a/spec/lib/gitlab/gitlab_import/client_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/client_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GitlabImport::Client do
+describe Gitlab::GitlabImport::Client, lib: true do
let(:token) { '123456' }
let(:client) { Gitlab::GitlabImport::Client.new(token) }
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 2d8923d14bb..483f65cd053 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GitlabImport::ProjectCreator do
+describe Gitlab::GitlabImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
let(:repo) do
{
diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
index c1125ca6357..946712ca38e 100644
--- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GitoriousImport::ProjectCreator do
+describe Gitlab::GitoriousImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
let(:namespace){ create(:group, owner: user) }
diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb
index 37985c062b4..85949ae8dc4 100644
--- a/spec/lib/gitlab/google_code_import/client_spec.rb
+++ b/spec/lib/gitlab/google_code_import/client_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Gitlab::GoogleCodeImport::Client do
+describe Gitlab::GoogleCodeImport::Client, lib: true do
let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) }
subject { described_class.new(raw_data) }
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
index 65ad7524cc2..647631271e0 100644
--- a/spec/lib/gitlab/google_code_import/importer_spec.rb
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Gitlab::GoogleCodeImport::Importer do
+describe Gitlab::GoogleCodeImport::Importer, lib: true do
let(:mapped_user) { create(:user, username: "thilo123") }
let(:raw_data) { JSON.parse(fixture_file("GoogleCodeProjectHosting.json")) }
let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) }
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
index 35549b48687..499a896ee76 100644
--- a/spec/lib/gitlab/google_code_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::GoogleCodeImport::ProjectCreator do
+describe Gitlab::GoogleCodeImport::ProjectCreator, lib: true do
let(:user) { create(:user) }
let(:repo) do
Gitlab::GoogleCodeImport::Repository.new(
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
index 5fdb9c723b1..bcdba8d4c12 100644
--- a/spec/lib/gitlab/incoming_email_spec.rb
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Gitlab::IncomingEmail do
+describe Gitlab::IncomingEmail, lib: true do
describe "self.enabled?" do
context "when reply by email is enabled" do
before do
diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb
index 2e0a05088cc..c690c195112 100644
--- a/spec/lib/gitlab/diff/inline_diff_spec.rb
+++ b/spec/lib/gitlab/inline_diff_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::InlineDiff do
+describe Gitlab::InlineDiff, lib: true do
describe '#processing' do
let(:diff) do
<<eos
diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb
index 266eab6e793..d09f51f3bfc 100644
--- a/spec/lib/gitlab/key_fingerprint_spec.rb
+++ b/spec/lib/gitlab/key_fingerprint_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe Gitlab::KeyFingerprint do
+describe Gitlab::KeyFingerprint, lib: true do
let(:key) { "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" }
let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" }
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index c38f212b405..a628d0c0157 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Access do
+describe Gitlab::LDAP::Access, lib: true do
let(:access) { Gitlab::LDAP::Access.new user }
let(:user) { create(:omniauth_user) }
@@ -13,6 +13,11 @@ describe Gitlab::LDAP::Access do
end
it { is_expected.to be_falsey }
+
+ it 'should block user in GitLab' do
+ access.allowed?
+ expect(user).to be_blocked
+ end
end
context 'when the user is found' do
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb
index 38076602df9..4847b5f3b0e 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/ldap/adapter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Adapter do
+describe Gitlab::LDAP::Adapter, lib: true do
let(:adapter) { Gitlab::LDAP::Adapter.new 'ldapmain' }
describe '#dn_matches_filter?' do
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
index 7d8268536a4..6a53ed1db64 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::AuthHash do
+describe Gitlab::LDAP::AuthHash, lib: true do
let(:auth_hash) do
Gitlab::LDAP::AuthHash.new(
OmniAuth::AuthHash.new(
diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/ldap/authentication_spec.rb
index 6e3de914a45..b8f3290e84c 100644
--- a/spec/lib/gitlab/ldap/authentication_spec.rb
+++ b/spec/lib/gitlab/ldap/authentication_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Authentication do
+describe Gitlab::LDAP::Authentication, lib: true do
let(:user) { create(:omniauth_user, extern_uid: dn) }
let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
let(:login) { 'john' }
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 3548d647c84..835853a83a4 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Config do
+describe Gitlab::LDAP::Config, lib: true do
let(:config) { Gitlab::LDAP::Config.new provider }
let(:provider) { 'ldapmain' }
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index b5b56a34952..1e755259dae 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::User do
+describe Gitlab::LDAP::User, lib: true do
let(:ldap_user) { Gitlab::LDAP::User.new(auth_hash) }
let(:gl_user) { ldap_user.gl_user }
let(:info) do
@@ -42,6 +42,21 @@ describe Gitlab::LDAP::User do
end
end
+ describe '.find_by_uid_and_provider' do
+ it 'retrieves the correct user' do
+ special_info = {
+ name: 'John Åström',
+ email: 'john@example.com',
+ nickname: 'jastrom'
+ }
+ special_hash = OmniAuth::AuthHash.new(uid: 'CN=John Åström,CN=Users,DC=Example,DC=com', provider: 'ldapmain', info: special_info)
+ special_chars_user = described_class.new(special_hash)
+ user = special_chars_user.save
+
+ expect(described_class.find_by_uid_and_provider(special_hash.uid, special_hash.provider)).to eq user
+ end
+ end
+
describe :find_or_create do
it "finds the user if already existing" do
create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
new file mode 100644
index 00000000000..5852b31ab3a
--- /dev/null
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -0,0 +1,765 @@
+require 'spec_helper'
+
+describe Gitlab::Lfs::Router, lib: true do
+ let(:project) { create(:project) }
+ let(:public_project) { create(:project, :public) }
+ let(:forked_project) { fork_project(public_project, user) }
+
+ let(:user) { create(:user) }
+ let(:user_two) { create(:user) }
+ let!(:lfs_object) { create(:lfs_object, :with_file) }
+
+ let(:request) { Rack::Request.new(env) }
+ let(:env) do
+ {
+ 'rack.input' => '',
+ 'REQUEST_METHOD' => 'GET',
+ }
+ 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(: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\"}"]]}
+
+ describe 'when lfs is disabled' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ env['REQUEST_METHOD'] = 'POST'
+ body = {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ],
+ 'operation' => 'upload'
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
+ end
+
+ it 'responds with 501' do
+ expect(lfs_router_auth.try_call).to match_array(respond_with_disabled)
+ end
+ end
+
+ describe 'when fetching lfs object using deprecated API' do
+ before do
+ enable_lfs
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
+ end
+
+ it 'responds with 501' do
+ expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
+ end
+ end
+
+ describe 'when fetching lfs object' do
+ before do
+ enable_lfs
+ env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}"
+ end
+
+ describe 'and request comes from gitlab-workhorse' do
+ context 'without user being authorized' do
+ it "responds with status 401" do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'with required headers' do
+ before do
+ env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
+ end
+
+ context 'when user does not have project access' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'when user has project access' do
+ before do
+ project.lfs_objects << lfs_object
+ project.team << [user, :master]
+ end
+
+ it "responds with status 200" do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with the file location" do
+ expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
+ expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
+ end
+
+ context 'without required headers' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+ end
+
+ describe 'when handling lfs request using deprecated API' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'POST'
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects"
+ end
+
+ it 'responds with 501' do
+ expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated)
+ end
+ end
+
+ describe 'when handling lfs batch request' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'POST'
+ env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
+ 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
+
+ describe 'when user has download access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :reporter]
+ 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)
+
+ 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
+
+ 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)
+
+ 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
+
+ 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)
+
+ 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
+
+ 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)
+
+ 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
+
+ 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
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'when user does not have download access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :guest]
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ context 'when user is not authenticated' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }],
+
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ describe 'is accessing public project' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 200 and href to download' do
+ response = lfs_router_public_noauth.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' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => {}
+ }
+ }
+ }])
+ end
+ end
+
+ describe 'is accessing non-public project' do
+ before do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with authorization required' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ 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
+
+ describe 'when user has project push access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :developer]
+ end
+
+ context 'when pushing an lfs object that already exists' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it "responds with status 200 and links the object to the project" do
+ response_body = lfs_router_auth.try_call.last
+ response = ActiveSupport::JSON.decode(response_body.first)
+
+ 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)).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)
+ end
+ end
+
+ context 'when pushing a lfs object that does not exist' do
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ it "responds with status 200 and upload hypermedia link" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+ expect(response_body['objects']).to be_kind_of(Array)
+ expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(response_body['objects'].first['size']).to eq(1575078)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
+ expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth)
+ end
+ end
+
+ context 'when pushing one new and one existing lfs object' do
+ before do
+ body = { 'operation' => 'upload',
+ '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)
+ expect(response_body['objects']).to be_kind_of(Array)
+
+ expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(response_body['objects'].first['size']).to eq(1575078)
+ expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth)
+
+ 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')
+ end
+ end
+ end
+
+ context 'when user does not have push access' do
+ it 'responds with 403' 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
+ )
+ end
+
+ context 'when user has push access' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it "responds with status 401" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'when user does not have push access' do
+ it "responds with status 401" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'unsupported' do
+ before do
+ body = { 'operation' => 'other',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ it 'responds with status 404' do
+ expect(lfs_router_public_noauth.try_call.first).to eq(404)
+ end
+ end
+ end
+
+ describe 'when pushing a lfs object' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'PUT'
+ end
+
+ describe 'to one project' do
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :master]
+ 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)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
+
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ 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 200 and lfs object is linked to the project' do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ 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 '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
+ 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
+
+ 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 'responds with 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 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+ end
+
+ 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
+ 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 200, location of lfs store and object details' do
+ json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
+
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ 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 status 200 and lfs object is linked to the source project' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
+ 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 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
+
+ it 'responds with status 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+ 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
+
+ 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
+
+ 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 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ 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) }
+
+ before do
+ public_project.lfs_objects << lfs_object
+ headers_for_upload_finalize(second_project)
+ end
+
+ context 'when pushing the same lfs object to the second project' do
+ before do
+ second_project.team << [user, :master]
+ end
+
+ it 'responds with 200 and links the lfs object to the project' do
+ expect(lfs_router_second_project.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id)
+ end
+ end
+ end
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ def authorize(user)
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ end
+
+ def new_lfs_router(project, user)
+ Gitlab::Lfs::Router.new(project, user, request)
+ end
+
+ def header_for_upload_authorize(project)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize"
+ end
+
+ def headers_for_upload_finalize(project)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4"
+ end
+
+ def fork_project(project, user, object = nil)
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ Projects::ForkService.new(project, user, {}).execute
+ end
+end
diff --git a/spec/lib/gitlab/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb
deleted file mode 100644
index 26332ba5217..00000000000
--- a/spec/lib/gitlab/markdown/autolink_filter_spec.rb
+++ /dev/null
@@ -1,114 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe AutolinkFilter do
- include FilterSpecHelper
-
- let(:link) { 'http://about.gitlab.com/' }
-
- it 'does nothing when :autolink is false' do
- exp = act = link
- expect(filter(act, autolink: false).to_html).to eq exp
- end
-
- it 'does nothing with non-link text' do
- exp = act = 'This text contains no links to autolink'
- expect(filter(act).to_html).to eq exp
- end
-
- context 'Rinku schemes' do
- it 'autolinks http' do
- doc = filter("See #{link}")
- expect(doc.at_css('a').text).to eq link
- expect(doc.at_css('a')['href']).to eq link
- end
-
- it 'autolinks https' do
- link = 'https://google.com/'
- doc = filter("See #{link}")
-
- expect(doc.at_css('a').text).to eq link
- expect(doc.at_css('a')['href']).to eq link
- end
-
- it 'autolinks ftp' do
- link = 'ftp://ftp.us.debian.org/debian/'
- doc = filter("See #{link}")
-
- expect(doc.at_css('a').text).to eq link
- expect(doc.at_css('a')['href']).to eq link
- end
-
- it 'autolinks short URLs' do
- link = 'http://localhost:3000/'
- doc = filter("See #{link}")
-
- expect(doc.at_css('a').text).to eq link
- expect(doc.at_css('a')['href']).to eq link
- end
-
- it 'accepts link_attr options' do
- doc = filter("See #{link}", link_attr: { class: 'custom' })
-
- expect(doc.at_css('a')['class']).to eq 'custom'
- end
-
- described_class::IGNORE_PARENTS.each do |elem|
- it "ignores valid links contained inside '#{elem}' element" do
- exp = act = "<#{elem}>See #{link}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
- end
-
- context 'other schemes' do
- let(:link) { 'foo://bar.baz/' }
-
- it 'autolinks smb' do
- link = 'smb:///Volumes/shared/foo.pdf'
- doc = filter("See #{link}")
-
- expect(doc.at_css('a').text).to eq link
- expect(doc.at_css('a')['href']).to eq link
- end
-
- it 'autolinks irc' do
- link = 'irc://irc.freenode.net/git'
- doc = filter("See #{link}")
-
- expect(doc.at_css('a').text).to eq link
- expect(doc.at_css('a')['href']).to eq link
- end
-
- it 'does not include trailing punctuation' do
- doc = filter("See #{link}.")
- expect(doc.at_css('a').text).to eq link
-
- doc = filter("See #{link}, ok?")
- expect(doc.at_css('a').text).to eq link
-
- doc = filter("See #{link}...")
- expect(doc.at_css('a').text).to eq link
- end
-
- it 'does not include trailing HTML entities' do
- doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
-
- expect(doc.at_css('a')['href']).to eq link
- expect(doc.text).to eq "See <<<#{link}>>>"
- end
-
- it 'accepts link_attr options' do
- doc = filter("See #{link}", link_attr: { class: 'custom' })
- expect(doc.at_css('a')['class']).to eq 'custom'
- end
-
- described_class::IGNORE_PARENTS.each do |elem|
- it "ignores valid links contained inside '#{elem}' element" do
- exp = act = "<#{elem}>See #{link}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
deleted file mode 100644
index e5b8d723fe5..00000000000
--- a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
+++ /dev/null
@@ -1,145 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe CommitRangeReferenceFilter do
- include FilterSpecHelper
-
- let(:project) { create(:project, :public) }
- let(:commit1) { project.commit }
- let(:commit2) { project.commit("HEAD~2") }
-
- let(:range) { CommitRange.new("#{commit1.id}...#{commit2.id}") }
- let(:range2) { CommitRange.new("#{commit1.id}..#{commit2.id}") }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Commit Range #{range.to_reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'internal reference' do
- let(:reference) { range.to_reference }
- let(:reference2) { range2.to_reference }
-
- it 'links to a valid two-dot reference' do
- doc = filter("See #{reference2}")
-
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_compare_url(project.namespace, project, range2.to_param)
- end
-
- it 'links to a valid three-dot reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_compare_url(project.namespace, project, range.to_param)
- end
-
- it 'links to a valid short ID' do
- reference = "#{commit1.short_id}...#{commit2.id}"
- reference2 = "#{commit1.id}...#{commit2.short_id}"
-
- exp = commit1.short_id + '...' + commit2.short_id
-
- expect(filter("See #{reference}").css('a').first.text).to eq exp
- expect(filter("See #{reference2}").css('a').first.text).to eq exp
- end
-
- it 'links with adjacent text' do
- doc = filter("See (#{reference}.)")
-
- exp = Regexp.escape(range.to_s)
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
- end
-
- it 'ignores invalid commit IDs' do
- exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
-
- expect(project).to receive(:valid_repo?).and_return(true)
- expect(project.repository).to receive(:commit).with(commit1.id.reverse)
- expect(filter(act).to_html).to eq exp
- end
-
- it 'includes a title attribute' do
- doc = filter("See #{reference}")
- expect(doc.css('a').first.attr('title')).to eq range.reference_title
- end
-
- it 'includes default classes' do
- doc = filter("See #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
- end
-
- it 'includes a data-project attribute' do
- doc = filter("See #{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-commit-range attribute' do
- doc = filter("See #{reference}")
- link = doc.css('a').first
-
- expect(link).to have_attribute('data-commit-range')
- expect(link.attr('data-commit-range')).to eq range.to_reference
- end
-
- it 'supports an :only_path option' do
- doc = filter("See #{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_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
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, :public, namespace: namespace) }
- let(:reference) { range.to_reference(project) }
-
- before do
- range.project = project2
- end
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_compare_url(project2.namespace, project2, range.to_param)
- end
-
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
-
- exp = Regexp.escape("#{project2.to_reference}@#{range.to_s}")
- expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
- end
-
- it 'ignores invalid commit IDs on the referenced project' do
- exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}"
- expect(filter(act).to_html).to eq exp
-
- exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}"
- expect(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
-end
diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
deleted file mode 100644
index d080efbf3d4..00000000000
--- a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
+++ /dev/null
@@ -1,135 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe CommitReferenceFilter do
- include FilterSpecHelper
-
- let(:project) { create(:project, :public) }
- let(:commit) { project.commit }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'internal reference' do
- let(:reference) { commit.id }
-
- # Let's test a variety of commit SHA sizes just to be paranoid
- [6, 8, 12, 18, 20, 32, 40].each do |size|
- it "links to a valid reference of #{size} characters" do
- doc = filter("See #{reference[0...size]}")
-
- expect(doc.css('a').first.text).to eq commit.short_id
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_commit_url(project.namespace, project, reference)
- end
- end
-
- it 'always uses the short ID as the link text' do
- doc = filter("See #{commit.id}")
- expect(doc.text).to eq "See #{commit.short_id}"
-
- doc = filter("See #{commit.id[0...6]}")
- expect(doc.text).to eq "See #{commit.short_id}"
- end
-
- it 'links with adjacent text' do
- doc = filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
- end
-
- it 'ignores invalid commit IDs' do
- invalid = invalidate_reference(reference)
- exp = act = "See #{invalid}"
-
- expect(project).to receive(:valid_repo?).and_return(true)
- expect(project.repository).to receive(:commit).with(invalid)
- expect(filter(act).to_html).to eq exp
- end
-
- it 'includes a title attribute' do
- doc = filter("See #{reference}")
- expect(doc.css('a').first.attr('title')).to eq commit.link_title
- end
-
- it 'escapes the title attribute' do
- allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
-
- doc = filter("See #{reference}")
- expect(doc.text).to eq "See #{commit.short_id}"
- end
-
- it 'includes default classes' do
- doc = filter("See #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
- end
-
- it 'includes a data-project attribute' do
- doc = filter("See #{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-commit attribute' do
- doc = filter("See #{reference}")
- link = doc.css('a').first
-
- expect(link).to have_attribute('data-commit')
- expect(link.attr('data-commit')).to eq commit.id
- end
-
- it 'supports an :only_path context' do
- doc = filter("See #{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_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
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, :public, namespace: namespace) }
- let(:commit) { project2.commit }
- let(:reference) { commit.to_reference(project) }
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
- end
-
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
-
- exp = Regexp.escape(project2.to_reference)
- expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
- end
-
- it 'ignores invalid commit IDs on the referenced project' do
- exp = act = "Committed #{invalidate_reference(reference)}"
- expect(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
- end
-end
diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
deleted file mode 100644
index 8d4f9e403a6..00000000000
--- a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe CrossProjectReference do
- include described_class
-
- describe '#project_from_ref' do
- context 'when no project was referenced' do
- it 'returns the project from context' do
- project = double
-
- allow(self).to receive(:context).and_return({ project: project })
-
- expect(project_from_ref(nil)).to eq project
- end
- end
-
- context 'when referenced project does not exist' do
- it 'returns nil' do
- expect(project_from_ref('invalid/reference')).to be_nil
- end
- end
-
- context 'when referenced project exists' do
- it 'returns the referenced project' do
- project2 = double('referenced project')
-
- expect(Project).to receive(:find_with_namespace).
- with('cross/reference').and_return(project2)
-
- expect(project_from_ref('cross/reference')).to eq project2
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb
deleted file mode 100644
index 11efd9bb4cd..00000000000
--- a/spec/lib/gitlab/markdown/emoji_filter_spec.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe EmojiFilter do
- include FilterSpecHelper
-
- before do
- ActionController::Base.asset_host = 'https://foo.com'
- end
-
- it 'replaces supported emoji' do
- doc = filter('<p>:heart:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
- end
-
- it 'ignores unsupported emoji' do
- exp = act = '<p>:foo:</p>'
- doc = filter(act)
- expect(doc.to_html).to match Regexp.escape(exp)
- end
-
- it 'correctly encodes the URL' do
- doc = filter('<p>:+1:</p>')
- expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
- end
-
- it 'matches at the start of a string' do
- doc = filter(':+1:')
- expect(doc.css('img').size).to eq 1
- end
-
- it 'matches at the end of a string' do
- doc = filter('This gets a :-1:')
- expect(doc.css('img').size).to eq 1
- end
-
- it 'matches with adjacent text' do
- doc = filter('+1 (:+1:)')
- expect(doc.css('img').size).to eq 1
- end
-
- it 'matches multiple emoji in a row' do
- doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
- expect(doc.css('img').size).to eq 3
- end
-
- it 'has a title attribute' do
- doc = filter(':-1:')
- expect(doc.css('img').first.attr('title')).to eq ':-1:'
- end
-
- it 'has an alt attribute' do
- doc = filter(':-1:')
- expect(doc.css('img').first.attr('alt')).to eq ':-1:'
- end
-
- it 'has an align attribute' do
- doc = filter(':8ball:')
- expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
- end
-
- it 'has an emoji class' do
- doc = filter(':cat:')
- expect(doc.css('img').first.attr('class')).to eq 'emoji'
- end
-
- it 'has height and width attributes' do
- doc = filter(':dog:')
- img = doc.css('img').first
-
- expect(img.attr('width')).to eq '20'
- expect(img.attr('height')).to eq '20'
- end
-
- it 'keeps whitespace intact' do
- doc = filter('This deserves a :+1:, big time.')
-
- expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
- end
-
- it 'uses a custom asset_root context' do
- root = Gitlab.config.gitlab.url + 'gitlab/root'
-
- doc = filter(':smile:', asset_root: root)
- expect(doc.css('img').first.attr('src')).to start_with(root)
- end
-
- it 'uses a custom asset_host context' do
- ActionController::Base.asset_host = 'https://cdn.example.com'
-
- doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
- expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
deleted file mode 100644
index d8c2970b6bd..00000000000
--- a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe ExternalIssueReferenceFilter do
- include FilterSpecHelper
-
- def helper
- IssuesHelper
- end
-
- let(:project) { create(:jira_project) }
-
- context 'JIRA issue references' do
- let(:issue) { ExternalIssue.new('JIRA-123', project) }
- let(:reference) { issue.to_reference }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- it 'ignores valid references when using default tracker' do
- expect(project).to receive(:default_issues_tracker?).and_return(true)
-
- exp = act = "Issue #{reference}"
- expect(filter(act).to_html).to eq exp
- end
-
- it 'links to a valid reference' do
- doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('href'))
- .to eq helper.url_for_issue(reference, project)
- end
-
- it 'links to the external tracker' do
- doc = filter("Issue #{reference}")
- link = doc.css('a').first.attr('href')
-
- expect(link).to eq "http://jira.example/browse/#{reference}"
- end
-
- it 'links with adjacent text' do
- doc = filter("Issue (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
- end
-
- it 'includes a title attribute' do
- doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
- end
-
- it 'escapes the title attribute' do
- allow(project.external_issue_tracker).to receive(:title).
- and_return(%{"></a>whatever<a title="})
-
- doc = filter("Issue #{reference}")
- expect(doc.text).to eq "Issue #{reference}"
- end
-
- it 'includes default classes' do
- doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
- end
-
- it 'supports an :only_path context' do
- doc = filter("Issue #{reference}", only_path: true)
- link = doc.css('a').first.attr('href')
-
- expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/external_link_filter_spec.rb b/spec/lib/gitlab/markdown/external_link_filter_spec.rb
deleted file mode 100644
index a040b34577b..00000000000
--- a/spec/lib/gitlab/markdown/external_link_filter_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe ExternalLinkFilter do
- include FilterSpecHelper
-
- it 'ignores elements without an href attribute' do
- exp = act = %q(<a id="ignored">Ignore Me</a>)
- expect(filter(act).to_html).to eq exp
- end
-
- it 'ignores non-HTTP(S) links' do
- exp = act = %q(<a href="irc://irc.freenode.net/gitlab">IRC</a>)
- expect(filter(act).to_html).to eq exp
- end
-
- it 'skips internal links' do
- internal = Gitlab.config.gitlab.url
- exp = act = %Q(<a href="#{internal}/sign_in">Login</a>)
- 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 eq 'nofollow'
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
deleted file mode 100644
index 94c80ae6611..00000000000
--- a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
+++ /dev/null
@@ -1,139 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe IssueReferenceFilter do
- include FilterSpecHelper
-
- def helper
- IssuesHelper
- end
-
- let(:project) { create(:empty_project, :public) }
- let(:issue) { create(:issue, project: project) }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Issue #{issue.to_reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'internal reference' 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)
-
- exp = act = "Issue #{reference}"
- expect(filter(act).to_html).to eq exp
- end
-
- it 'links to a valid reference' do
- doc = filter("Fixed #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq helper.url_for_issue(issue.iid, project)
- end
-
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
-
- it 'ignores invalid issue IDs' do
- invalid = invalidate_reference(reference)
- exp = act = "Fixed #{invalid}"
-
- expect(filter(act).to_html).to eq exp
- end
-
- it 'includes a title attribute' do
- doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
- end
-
- it 'escapes the title attribute' do
- issue.update_attribute(:title, %{"></a>whatever<a title="})
-
- doc = filter("Issue #{reference}")
- expect(doc.text).to eq "Issue #{reference}"
- end
-
- it 'includes default classes' do
- doc = filter("Issue #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
- end
-
- it 'includes a data-project attribute' do
- doc = filter("Issue #{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-issue attribute' do
- doc = filter("See #{reference}")
- link = doc.css('a').first
-
- expect(link).to have_attribute('data-issue')
- expect(link.attr('data-issue')).to eq issue.id.to_s
- end
-
- it 'supports an :only_path context' do
- doc = filter("Issue #{reference}", only_path: true)
- link = doc.css('a').first.attr('href')
-
- expect(link).not_to match %r(https?://)
- 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
- end
-
- context 'cross-project reference' do
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:empty_project, :public, namespace: namespace) }
- let(:issue) { create(:issue, project: project2) }
- 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)
-
- exp = act = "Issue #{reference}"
- expect(filter(act).to_html).to eq exp
- end
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq helper.url_for_issue(issue.iid, project2)
- end
-
- it 'links with adjacent text' do
- doc = filter("Fixed (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
-
- it 'ignores invalid issue IDs on the referenced project' do
- exp = act = "Fixed #{invalidate_reference(reference)}"
-
- expect(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]
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
deleted file mode 100644
index fc21b65a843..00000000000
--- a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
+++ /dev/null
@@ -1,144 +0,0 @@
-require 'spec_helper'
-require 'html/pipeline'
-
-module Gitlab::Markdown
- describe LabelReferenceFilter do
- include FilterSpecHelper
-
- let(:project) { create(:empty_project, :public) }
- let(:label) { create(:label, project: project) }
- let(:reference) { label.to_reference }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Label #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- it 'includes default classes' do
- doc = filter("Label #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
- end
-
- it 'includes a data-project attribute' do
- doc = filter("Label #{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-label attribute' do
- doc = filter("See #{reference}")
- link = doc.css('a').first
-
- expect(link).to have_attribute('data-label')
- expect(link.attr('data-label')).to eq label.id.to_s
- end
-
- it 'supports an :only_path context' do
- doc = filter("Label #{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_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 = filter("Label #{reference}")
- expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
- end
-
- it 'includes a style attribute' do
- doc = filter("Label #{reference}")
- expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
- end
- end
-
- context 'Integer-based references' do
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).to eq urls.
- namespace_project_issues_url(project.namespace, project, label_name: label.name)
- end
-
- it 'links with adjacent text' do
- doc = filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
- end
-
- it 'ignores invalid label IDs' do
- exp = act = "Label #{invalidate_reference(reference)}"
-
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'String-based single-word references' do
- let(:label) { create(:label, name: 'gfm', project: project) }
- let(:reference) { "#{Label.reference_prefix}#{label.name}" }
-
- it 'links to a valid reference' do
- doc = 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 = filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
- end
-
- it 'ignores invalid label names' do
- exp = act = "Label #{Label.reference_prefix}#{label.name.reverse}"
-
- expect(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(:name) }
-
- it 'links to a valid reference' do
- doc = 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 = filter("Label (#{reference}.)")
- expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
- end
-
- it 'ignores invalid label names' do
- exp = act = %(Label #{Label.reference_prefix}"#{label.name.reverse}")
-
- expect(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'
- expect(filter(act).to_html).to eq exp
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
deleted file mode 100644
index 3ef6cdfff33..00000000000
--- a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
+++ /dev/null
@@ -1,120 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe MergeRequestReferenceFilter do
- include FilterSpecHelper
-
- let(:project) { create(:project, :public) }
- let(:merge) { create(:merge_request, source_project: project) }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Merge #{merge.to_reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'internal reference' do
- let(:reference) { merge.to_reference }
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).to eq urls.
- namespace_project_merge_request_url(project.namespace, project, merge)
- end
-
- it 'links with adjacent text' do
- doc = filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
-
- it 'ignores invalid merge IDs' do
- exp = act = "Merge #{invalidate_reference(reference)}"
-
- expect(filter(act).to_html).to eq exp
- end
-
- it 'includes a title attribute' do
- doc = filter("Merge #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
- end
-
- it 'escapes the title attribute' do
- merge.update_attribute(:title, %{"></a>whatever<a title="})
-
- doc = filter("Merge #{reference}")
- expect(doc.text).to eq "Merge #{reference}"
- end
-
- it 'includes default classes' do
- doc = filter("Merge #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
- end
-
- it 'includes a data-project attribute' do
- doc = filter("Merge #{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-merge-request attribute' do
- doc = filter("See #{reference}")
- link = doc.css('a').first
-
- expect(link).to have_attribute('data-merge-request')
- expect(link.attr('data-merge-request')).to eq merge.id.to_s
- end
-
- it 'supports an :only_path context' do
- doc = filter("Merge #{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_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
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:project, :public, namespace: namespace) }
- let(:merge) { create(:merge_request, source_project: project2) }
- let(:reference) { merge.to_reference(project) }
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_merge_request_url(project2.namespace,
- project, merge)
- end
-
- it 'links with adjacent text' do
- doc = filter("Merge (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
-
- it 'ignores invalid merge IDs on the referenced project' do
- exp = act = "Merge #{invalidate_reference(reference)}"
-
- expect(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
- end
-end
diff --git a/spec/lib/gitlab/markdown/redactor_filter_spec.rb b/spec/lib/gitlab/markdown/redactor_filter_spec.rb
deleted file mode 100644
index eea3f1cf370..00000000000
--- a/spec/lib/gitlab/markdown/redactor_filter_spec.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe RedactorFilter do
- include ActionView::Helpers::UrlHelper
- include FilterSpecHelper
-
- it 'ignores non-GFM links' do
- html = %(See <a href="https://google.com/">Google</a>)
- doc = filter(html, current_user: double)
-
- expect(doc.css('a').length).to eq 1
- end
-
- def reference_link(data)
- link_to('text', '', class: 'gfm', data: data)
- end
-
- context 'with data-project' do
- it 'removes unpermitted Project references' do
- user = create(:user)
- project = create(:empty_project)
-
- link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
- doc = filter(link, current_user: user)
-
- expect(doc.css('a').length).to eq 0
- end
-
- it 'allows permitted Project references' do
- user = create(:user)
- project = create(:empty_project)
- project.team << [user, :master]
-
- link = reference_link(project: project.id, reference_filter: Gitlab::Markdown::ReferenceFilter.name)
- 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: Gitlab::Markdown::ReferenceFilter.name)
-
- expect { filter(link) }.not_to raise_error
- 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: Gitlab::Markdown::UserReferenceFilter.name)
- doc = filter(link, current_user: user)
-
- expect(doc.css('a').length).to eq 0
- 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: Gitlab::Markdown::UserReferenceFilter.name)
- 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: Gitlab::Markdown::UserReferenceFilter.name)
-
- expect { filter(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: Gitlab::Markdown::UserReferenceFilter.name)
- doc = filter(link)
-
- expect(doc.css('a').length).to eq 1
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb b/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
deleted file mode 100644
index 4fa473ad191..00000000000
--- a/spec/lib/gitlab/markdown/reference_gatherer_filter_spec.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe ReferenceGathererFilter 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: Gitlab::Markdown::IssueReferenceFilter.name)
- 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: Gitlab::Markdown::IssueReferenceFilter.name)
- 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: Gitlab::Markdown::IssueReferenceFilter.name)
-
- 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: Gitlab::Markdown::UserReferenceFilter.name)
- 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: Gitlab::Markdown::UserReferenceFilter.name)
- 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: Gitlab::Markdown::UserReferenceFilter.name)
-
- 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: Gitlab::Markdown::UserReferenceFilter.name)
- result = pipeline_result(link)
-
- expect(result[:references][:user]).to eq([user])
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
deleted file mode 100644
index 027336ceb73..00000000000
--- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
+++ /dev/null
@@ -1,149 +0,0 @@
-# encoding: UTF-8
-
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe RelativeLinkFilter do
- def filter(doc, contexts = {})
- contexts.reverse_merge!({
- commit: project.commit,
- project: project,
- project_wiki: project_wiki,
- ref: ref,
- requested_path: requested_path
- })
-
- described_class.call(doc, contexts)
- end
-
- def image(path)
- %(<img src="#{path}" />)
- end
-
- def link(path)
- %(<a href="#{path}">#{path}</a>)
- end
-
- let(:project) { create(:project) }
- let(:project_path) { project.path_with_namespace }
- let(:ref) { 'markdown' }
- let(:project_wiki) { nil }
- let(:requested_path) { '/' }
-
- shared_examples :preserve_unchanged do
- it 'does not modify any relative URL in anchor' do
- doc = filter(link('README.md'))
- expect(doc.at_css('a')['href']).to eq 'README.md'
- end
-
- it 'does not modify any relative URL in image' do
- doc = filter(image('files/images/logo-black.png'))
- expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
- end
- end
-
- shared_examples :relative_to_requested do
- it 'rebuilds URL relative to the requested path' do
- doc = filter(link('users.md'))
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/blob/#{ref}/doc/api/users.md"
- end
- end
-
- context 'with a project_wiki' do
- let(:project_wiki) { double('ProjectWiki') }
- include_examples :preserve_unchanged
- end
-
- context 'without a repository' do
- let(:project) { create(:empty_project) }
- include_examples :preserve_unchanged
- end
-
- context 'with an empty repository' do
- let(:project) { create(:project_empty_repo) }
- include_examples :preserve_unchanged
- end
-
- it 'does not raise an exception on invalid URIs' do
- act = link("://foo")
- expect { filter(act) }.not_to raise_error
- end
-
- context 'with a valid repository' do
- it 'rebuilds relative URL for a file in the repo' do
- doc = filter(link('doc/api/README.md'))
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
- end
-
- it 'rebuilds relative URL for a file in the repo up one directory' do
- relative_link = link('../api/README.md')
- doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
-
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
- end
-
- it 'rebuilds relative URL for a file in the repo up multiple directories' do
- relative_link = link('../../../api/README.md')
- doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md')
-
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
- end
-
- it 'rebuilds relative URL for a file in the repo with an anchor' do
- doc = filter(link('README.md#section'))
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/blob/#{ref}/README.md#section"
- end
-
- it 'rebuilds relative URL for a directory in the repo' do
- doc = filter(link('doc/api/'))
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/tree/#{ref}/doc/api"
- end
-
- it 'rebuilds relative URL for an image in the repo' do
- doc = filter(link('files/images/logo-black.png'))
- expect(doc.at_css('a')['href']).
- to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png"
- end
-
- it 'does not modify relative URL with an anchor only' do
- doc = filter(link('#section-1'))
- expect(doc.at_css('a')['href']).to eq '#section-1'
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'))
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
-
- it 'supports Unicode filenames' 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)
-
- doc = filter(image(escaped))
- expect(doc.at_css('img')['src']).to match '/raw/'
- end
-
- context 'when requested path is a file in the repo' do
- let(:requested_path) { 'doc/api/README.md' }
- include_examples :relative_to_requested
- end
-
- context 'when requested path is a directory in the repo' do
- let(:requested_path) { 'doc/api' }
- include_examples :relative_to_requested
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
deleted file mode 100644
index e50c82d0b3c..00000000000
--- a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe SanitizationFilter do
- include FilterSpecHelper
-
- describe 'default whitelist' do
- it 'sanitizes tags that are not whitelisted' do
- act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
- exp = 'no inputs and no blinks'
- expect(filter(act).to_html).to eq exp
- end
-
- it 'sanitizes tag attributes' do
- act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
- exp = %q{<a href="http://example.com/bar.html">Text</a>}
- expect(filter(act).to_html).to eq exp
- end
-
- it 'sanitizes javascript in attributes' do
- act = %q(<a href="javascript:alert('foo')">Text</a>)
- exp = '<a>Text</a>'
- expect(filter(act).to_html).to eq exp
- end
-
- it 'allows whitelisted HTML tags from the user' do
- exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>"
- expect(filter(act).to_html).to eq exp
- end
-
- it 'sanitizes `class` attribute on any element' do
- act = %q{<strong class="foo">Strong</strong>}
- expect(filter(act).to_html).to eq %q{<strong>Strong</strong>}
- end
-
- it 'sanitizes `id` attribute on any element' do
- act = %q{<em id="foo">Emphasis</em>}
- expect(filter(act).to_html).to eq %q{<em>Emphasis</em>}
- end
- end
-
- describe 'custom whitelist' do
- it 'customizes the whitelist only once' do
- instance = described_class.new('Foo')
- 3.times { instance.whitelist }
-
- expect(instance.whitelist[:transformers].size).to eq 4
- end
-
- it 'allows syntax highlighting' do
- exp = act = %q{<pre class="code highlight white c"><code><span class="k">def</span></code></pre>}
- expect(filter(act).to_html).to eq exp
- end
-
- it 'sanitizes `class` attribute from non-highlight spans' do
- act = %q{<span class="k">def</span>}
- expect(filter(act).to_html).to eq %q{<span>def</span>}
- end
-
- it 'allows `style` attribute on table elements' do
- html = <<-HTML.strip_heredoc
- <table>
- <tr><th style="text-align: center">Head</th></tr>
- <tr><td style="text-align: right">Body</th></tr>
- </table>
- HTML
-
- doc = filter(html)
-
- expect(doc.at_css('th')['style']).to eq 'text-align: center'
- expect(doc.at_css('td')['style']).to eq 'text-align: right'
- end
-
- it 'allows `span` elements' do
- exp = act = %q{<span>Hello</span>}
- expect(filter(act).to_html).to eq exp
- end
-
- it 'removes `rel` attribute from `a` elements' do
- doc = filter(%q{<a href="#" rel="nofollow">Link</a>})
-
- expect(doc.css('a').size).to eq 1
- expect(doc.at_css('a')['href']).to eq '#'
- expect(doc.at_css('a')['rel']).to be_nil
- end
-
- it 'removes script-like `href` attribute from `a` elements' do
- html = %q{<a href="javascript:alert('Hi')">Hi</a>}
- doc = filter(html)
-
- expect(doc.css('a').size).to eq 1
- expect(doc.at_css('a')['href']).to be_nil
- end
- end
-
- context 'when pipeline is :description' do
- it 'uses a stricter whitelist' do
- doc = filter('<h1>Description</h1>', pipeline: :description)
- expect(doc.to_html.strip).to eq 'Description'
- end
-
- %w(pre code img ol ul li).each do |elem|
- it "removes '#{elem}' elements" do
- act = "<#{elem}>Description</#{elem}>"
- expect(filter(act, pipeline: :description).to_html.strip).
- to eq 'Description'
- end
- end
-
- %w(b i strong em a ins del sup sub p).each do |elem|
- it "still allows '#{elem}' elements" do
- exp = act = "<#{elem}>Description</#{elem}>"
- expect(filter(act, pipeline: :description).to_html).to eq exp
- end
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
deleted file mode 100644
index 9d9652dba46..00000000000
--- a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe SnippetReferenceFilter do
- include FilterSpecHelper
-
- let(:project) { create(:empty_project, :public) }
- let(:snippet) { create(:project_snippet, project: project) }
- let(:reference) { snippet.to_reference }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'internal reference' do
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).to eq urls.
- namespace_project_snippet_url(project.namespace, project, snippet)
- end
-
- it 'links with adjacent text' do
- doc = filter("Snippet (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
-
- it 'ignores invalid snippet IDs' do
- exp = act = "Snippet #{invalidate_reference(reference)}"
-
- expect(filter(act).to_html).to eq exp
- end
-
- it 'includes a title attribute' do
- doc = filter("Snippet #{reference}")
- expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
- end
-
- it 'escapes the title attribute' do
- snippet.update_attribute(:title, %{"></a>whatever<a title="})
-
- doc = filter("Snippet #{reference}")
- expect(doc.text).to eq "Snippet #{reference}"
- end
-
- it 'includes default classes' do
- doc = filter("Snippet #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
- end
-
- it 'includes a data-project attribute' do
- doc = filter("Snippet #{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-snippet attribute' do
- doc = filter("See #{reference}")
- link = doc.css('a').first
-
- expect(link).to have_attribute('data-snippet')
- expect(link.attr('data-snippet')).to eq snippet.id.to_s
- end
-
- it 'supports an :only_path context' do
- doc = filter("Snippet #{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_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
- let(:namespace) { create(:namespace, name: 'cross-reference') }
- let(:project2) { create(:empty_project, :public, namespace: namespace) }
- let(:snippet) { create(:project_snippet, project: project2) }
- let(:reference) { snippet.to_reference(project) }
-
- it 'links to a valid reference' do
- doc = filter("See #{reference}")
-
- expect(doc.css('a').first.attr('href')).
- to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
- end
-
- it 'links with adjacent text' do
- doc = filter("See (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
- end
-
- it 'ignores invalid snippet IDs on the referenced project' do
- exp = act = "See #{invalidate_reference(reference)}"
-
- expect(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
- end
-end
diff --git a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
deleted file mode 100644
index 6a490673728..00000000000
--- a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe SyntaxHighlightFilter do
- include FilterSpecHelper
-
- it 'highlights valid code blocks' do
- result = filter('<pre><code>def fun end</code>')
- expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
- end
-
- it 'passes through invalid code blocks' do
- allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError)
-
- result = filter('<pre><code>This is a test</code></pre>')
- expect(result.to_html).to eq('<pre>This is a test</pre>')
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
deleted file mode 100644
index ddf583a72c1..00000000000
--- a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-# encoding: UTF-8
-
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe TableOfContentsFilter do
- include FilterSpecHelper
-
- def header(level, text)
- "<h#{level}>#{text}</h#{level}>\n"
- end
-
- it 'does nothing when :no_header_anchors is truthy' do
- exp = act = header(1, 'Header')
- expect(filter(act, no_header_anchors: 1).to_html).to eq exp
- end
-
- it 'does nothing with empty headers' do
- exp = act = header(1, nil)
- expect(filter(act).to_html).to eq exp
- end
-
- 1.upto(6) do |i|
- it "processes h#{i} elements" do
- html = header(i, "Header #{i}")
- doc = filter(html)
-
- expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
- end
- end
-
- describe 'anchor tag' do
- it 'has an `anchor` class' do
- doc = filter(header(1, 'Header'))
- expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
- end
-
- it 'links to the id' do
- doc = filter(header(1, 'Header'))
- expect(doc.css('h1 a').first.attr('href')).to eq '#header'
- end
-
- describe 'generated IDs' do
- it 'translates spaces to dashes' do
- doc = filter(header(1, 'This header has spaces in it'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
- end
-
- it 'squeezes multiple spaces and dashes' do
- doc = filter(header(1, 'This---header is poorly-formatted'))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
- end
-
- it 'removes punctuation' do
- doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
- expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
- end
-
- it 'appends a unique number to duplicates' do
- doc = filter(header(1, 'One') + header(2, 'One'))
-
- expect(doc.css('h1 a').first.attr('id')).to eq 'one'
- expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
- end
-
- it 'supports Unicode' do
- doc = filter(header(1, '한글'))
- expect(doc.css('h1 a').first.attr('id')).to eq '한글'
- expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
- end
- end
- end
-
- describe 'result' do
- def result(html)
- HTML::Pipeline.new([described_class]).call(html)
- end
-
- let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) }
- let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
-
- it 'is contained within a `ul` element' do
- expect(doc.children.first.name).to eq 'ul'
- expect(doc.children.first.attr('class')).to eq 'section-nav'
- end
-
- it 'contains an `li` element for each header' do
- expect(doc.css('li').length).to eq 2
-
- links = doc.css('li a')
-
- expect(links.first.attr('href')).to eq '#header-1'
- expect(links.first.text).to eq 'Header 1'
- expect(links.last.attr('href')).to eq '#header-2'
- expect(links.last.text).to eq 'Header 2'
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/task_list_filter_spec.rb b/spec/lib/gitlab/markdown/task_list_filter_spec.rb
deleted file mode 100644
index 94f39cc966e..00000000000
--- a/spec/lib/gitlab/markdown/task_list_filter_spec.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe TaskListFilter do
- include FilterSpecHelper
-
- it 'does not apply `task-list` class to non-task lists' do
- exp = act = %(<ul><li>Item</li></ul>)
- expect(filter(act).to_html).to eq exp
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb b/spec/lib/gitlab/markdown/upload_link_filter_spec.rb
deleted file mode 100644
index 9ae45a6f559..00000000000
--- a/spec/lib/gitlab/markdown/upload_link_filter_spec.rb
+++ /dev/null
@@ -1,75 +0,0 @@
-# encoding: UTF-8
-
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe UploadLinkFilter do
- def filter(doc, contexts = {})
- contexts.reverse_merge!({
- project: project
- })
-
- described_class.call(doc, contexts)
- end
-
- def image(path)
- %(<img src="#{path}" />)
- end
-
- def link(path)
- %(<a href="#{path}">#{path}</a>)
- end
-
- let(:project) { create(:project) }
-
- shared_examples :preserve_unchanged do
- it 'does not modify any relative URL in anchor' do
- doc = filter(link('README.md'))
- expect(doc.at_css('a')['href']).to eq 'README.md'
- end
-
- it 'does not modify any relative URL in image' do
- doc = filter(image('files/images/logo-black.png'))
- expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png'
- end
- end
-
- it 'does not raise an exception on invalid URIs' do
- act = link("://foo")
- expect { filter(act) }.not_to raise_error
- end
-
- context 'with a valid repository' do
- it 'rebuilds relative URL for a link' 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"
- end
-
- it 'rebuilds relative URL for an image' 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"
- end
-
- it 'does not modify absolute URL' do
- doc = filter(link('http://example.com'))
- expect(doc.at_css('a')['href']).to eq 'http://example.com'
- end
-
- it 'supports Unicode filenames' do
- path = '/uploads/한글.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)
-
- doc = filter(image(escaped))
- expect(doc.at_css('img')['src']).to match "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/%ED%95%9C%EA%B8%80.png"
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
deleted file mode 100644
index d9e0d7c42db..00000000000
--- a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-require 'spec_helper'
-
-module Gitlab::Markdown
- describe UserReferenceFilter do
- include FilterSpecHelper
-
- let(:project) { create(:empty_project, :public) }
- let(:user) { create(:user) }
- let(:reference) { user.to_reference }
-
- it 'requires project context' do
- expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
- end
-
- it 'ignores invalid users' do
- exp = act = "Hey #{invalidate_reference(reference)}"
- expect(filter(act).to_html).to eq(exp)
- end
-
- %w(pre code a style).each do |elem|
- it "ignores valid references contained inside '#{elem}' element" do
- exp = act = "<#{elem}>Hey #{reference}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
-
- context 'mentioning @all' do
- let(:reference) { User.reference_prefix + 'all' }
-
- before do
- project.team << [project.creator, :developer]
- end
-
- it 'supports a special @all mention' do
- doc = filter("Hey #{reference}")
- 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
-
- it 'adds to the results hash' do
- result = reference_pipeline_result("Hey #{reference}")
- expect(result[:references][:user]).to eq [project.creator]
- end
- end
-
- context 'mentioning a user' do
- it 'links to a User' do
- doc = filter("Hey #{reference}")
- expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
- end
-
- it 'links to a User with a period' do
- user = create(:user, name: 'alphA.Beta')
-
- doc = filter("Hey #{user.to_reference}")
- expect(doc.css('a').length).to eq 1
- end
-
- it 'links to a User with an underscore' do
- user = create(:user, name: 'ping_pong_king')
-
- doc = filter("Hey #{user.to_reference}")
- expect(doc.css('a').length).to eq 1
- end
-
- it 'includes a data-user attribute' do
- doc = filter("Hey #{reference}")
- link = doc.css('a').first
-
- 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
- let(:group) { create(:group) }
- let(:reference) { group.to_reference }
-
- it 'links to the Group' do
- doc = filter("Hey #{reference}")
- expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
- end
-
- it 'includes a data-group attribute' do
- doc = filter("Hey #{reference}")
- link = doc.css('a').first
-
- 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
- doc = filter("Mention me (#{reference}.)")
- expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
- end
-
- it 'includes default classes' do
- doc = filter("Hey #{reference}")
- expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
- end
-
- it 'supports an :only_path context' do
- doc = filter("Hey #{reference}", only_path: true)
- link = doc.css('a').first.attr('href')
-
- expect(link).not_to match %r(https?://)
- expect(link).to eq urls.user_path(user)
- end
- end
-end
diff --git a/spec/lib/gitlab/markup_helper_spec.rb b/spec/lib/gitlab/markup_helper_spec.rb
index e610fab05da..93b91b849f2 100644
--- a/spec/lib/gitlab/markup_helper_spec.rb
+++ b/spec/lib/gitlab/markup_helper_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::MarkupHelper do
+describe Gitlab::MarkupHelper, lib: true do
describe '#markup?' do
%w(textile rdoc org creole wiki
mediawiki rst adoc ad asciidoc mdown md markdown).each do |type|
diff --git a/spec/lib/gitlab/metrics/delta_spec.rb b/spec/lib/gitlab/metrics/delta_spec.rb
new file mode 100644
index 00000000000..718387cdee1
--- /dev/null
+++ b/spec/lib/gitlab/metrics/delta_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Delta do
+ let(:delta) { described_class.new }
+
+ describe '#compared_with' do
+ it 'returns the delta as a Numeric' do
+ expect(delta.compared_with(5)).to eq(5)
+ end
+
+ it 'bases the delta on a previously used value' do
+ expect(delta.compared_with(5)).to eq(5)
+ expect(delta.compared_with(15)).to eq(10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
new file mode 100644
index 00000000000..2a37cd40dde
--- /dev/null
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -0,0 +1,240 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Instrumentation do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ before do
+ @dummy = Class.new do
+ def self.foo(text = 'foo')
+ text
+ end
+
+ def bar(text = 'bar')
+ text
+ end
+ end
+
+ allow(@dummy).to receive(:name).and_return('Dummy')
+ end
+
+ describe '.configure' do
+ it 'yields self' do
+ described_class.configure do |c|
+ expect(c).to eq(described_class)
+ end
+ end
+ end
+
+ describe '.instrument_method' do
+ describe 'with metrics enabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+
+ described_class.instrument_method(@dummy, :foo)
+ end
+
+ it 'renames the original method' do
+ expect(@dummy).to respond_to(:_original_foo)
+ end
+
+ it 'calls the instrumented method with the correct arguments' do
+ expect(@dummy.foo).to eq('foo')
+ end
+
+ it 'tracks the call duration upon calling the method' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(0)
+
+ 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')
+
+ @dummy.foo
+ end
+
+ it 'does not track method calls below a given duration threshold' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(100)
+
+ expect(transaction).to_not receive(:add_metric)
+
+ @dummy.foo
+ end
+ end
+
+ describe 'with metrics disabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
+ end
+
+ it 'does not instrument the method' do
+ described_class.instrument_method(@dummy, :foo)
+
+ expect(@dummy).to_not respond_to(:_original_foo)
+ end
+ end
+ end
+
+ describe '.instrument_instance_method' do
+ describe 'with metrics enabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+
+ described_class.
+ instrument_instance_method(@dummy, :bar)
+ end
+
+ it 'renames the original method' do
+ expect(@dummy.method_defined?(:_original_bar)).to eq(true)
+ end
+
+ it 'calls the instrumented method with the correct arguments' do
+ expect(@dummy.new.bar).to eq('bar')
+ end
+
+ it 'tracks the call duration upon calling the method' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(0)
+
+ 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')
+
+ @dummy.new.bar
+ end
+
+ it 'does not track method calls below a given duration threshold' do
+ allow(Gitlab::Metrics).to receive(:method_call_threshold).
+ and_return(100)
+
+ expect(transaction).to_not receive(:add_metric)
+
+ @dummy.new.bar
+ end
+ end
+
+ describe 'with metrics disabled' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
+ end
+
+ it 'does not instrument the method' do
+ described_class.
+ instrument_instance_method(@dummy, :bar)
+
+ expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ end
+ end
+ end
+
+ describe '.instrument_class_hierarchy' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+
+ @child1 = Class.new(@dummy) do
+ def self.child1_foo; end
+ def child1_bar; end
+ end
+
+ @child2 = Class.new(@child1) do
+ def self.child2_foo; end
+ def child2_bar; end
+ end
+ end
+
+ it 'recursively instruments a class hierarchy' do
+ described_class.instrument_class_hierarchy(@dummy)
+
+ expect(@child1).to respond_to(:_original_child1_foo)
+ expect(@child2).to respond_to(:_original_child2_foo)
+
+ expect(@child1.method_defined?(:_original_child1_bar)).to eq(true)
+ expect(@child2.method_defined?(:_original_child2_bar)).to eq(true)
+ end
+
+ it 'does not instrument the root module' do
+ described_class.instrument_class_hierarchy(@dummy)
+
+ expect(@dummy).to_not respond_to(:_original_foo)
+ expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ end
+ end
+
+ describe '.instrument_methods' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+ end
+
+ it 'instruments all public class methods' do
+ described_class.instrument_methods(@dummy)
+
+ expect(@dummy).to respond_to(:_original_foo)
+ end
+
+ it 'only instruments methods directly defined in the module' do
+ mod = Module.new do
+ def kittens
+ end
+ end
+
+ @dummy.extend(mod)
+
+ described_class.instrument_methods(@dummy)
+
+ expect(@dummy).to_not respond_to(:_original_kittens)
+ end
+
+ it 'can take a block to determine if a method should be instrumented' do
+ described_class.instrument_methods(@dummy) do
+ false
+ end
+
+ expect(@dummy).to_not respond_to(:_original_foo)
+ end
+ end
+
+ describe '.instrument_instance_methods' do
+ before do
+ allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
+ end
+
+ it 'instruments all public instance methods' do
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(@dummy.method_defined?(:_original_bar)).to eq(true)
+ end
+
+ it 'only instruments methods directly defined in the module' do
+ mod = Module.new do
+ def kittens
+ end
+ end
+
+ @dummy.include(mod)
+
+ described_class.instrument_instance_methods(@dummy)
+
+ expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
+ end
+
+ it 'can take a block to determine if a method should be instrumented' do
+ described_class.instrument_instance_methods(@dummy) do
+ false
+ end
+
+ expect(@dummy.method_defined?(:_original_bar)).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
new file mode 100644
index 00000000000..f718d536130
--- /dev/null
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Metric do
+ let(:metric) do
+ described_class.new('foo', { number: 10 }, { host: 'localtoast' })
+ end
+
+ describe '#series' do
+ subject { metric.series }
+
+ it { is_expected.to eq('foo') }
+ end
+
+ describe '#values' do
+ subject { metric.values }
+
+ it { is_expected.to eq({ number: 10 }) }
+ end
+
+ describe '#tags' do
+ subject { metric.tags }
+
+ it { is_expected.to eq({ host: 'localtoast' }) }
+ end
+
+ describe '#to_hash' do
+ it 'returns a Hash' do
+ expect(metric.to_hash).to be_an_instance_of(Hash)
+ end
+
+ describe 'the returned Hash' do
+ let(:hash) { metric.to_hash }
+
+ it 'includes the series' do
+ expect(hash[:series]).to eq('foo')
+ end
+
+ it 'includes the tags' do
+ expect(hash[:tags]).to be_an_instance_of(Hash)
+ end
+
+ it 'includes the values' do
+ expect(hash[:values]).to eq({ number: 10 })
+ end
+
+ it 'includes the timestamp' do
+ expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
new file mode 100644
index 00000000000..a143fe4cfcd
--- /dev/null
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -0,0 +1,63 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::RackMiddleware do
+ let(:app) { double(:app) }
+
+ let(:middleware) { described_class.new(app) }
+
+ let(:env) { { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo' } }
+
+ describe '#call' do
+ before do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+ end
+
+ it 'tracks a transaction' do
+ expect(app).to receive(:call).with(env).and_return('yay')
+
+ expect(middleware.call(env)).to eq('yay')
+ end
+
+ it 'tags a transaction with the name and action of a controller' do
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show')
+
+ env['action_controller.instance'] = controller
+
+ allow(app).to receive(:call).with(env)
+
+ expect(middleware).to receive(:tag_controller).
+ with(an_instance_of(Gitlab::Metrics::Transaction), env)
+
+ middleware.call(env)
+ end
+ end
+
+ describe '#transaction_from_env' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'returns a Transaction' do
+ expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
+ end
+
+ it 'tags the transaction with the request method and URI' do
+ expect(transaction.tags[:request_method]).to eq('GET')
+ expect(transaction.tags[:request_uri]).to eq('/foo')
+ end
+ end
+
+ describe '#tag_controller' do
+ let(:transaction) { middleware.transaction_from_env(env) }
+
+ it 'tags a transaction with the name and action of a controller' do
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show')
+
+ env['action_controller.instance'] = controller
+
+ middleware.tag_controller(transaction, env)
+
+ expect(transaction.tags[:action]).to eq('TestController#show')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
new file mode 100644
index 00000000000..27211350fbe
--- /dev/null
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Sampler do
+ let(:sampler) { described_class.new(5) }
+
+ after do
+ Allocations.stop if Gitlab::Metrics.mri?
+ end
+
+ describe '#start' do
+ it 'gathers a sample at a given interval' do
+ expect(sampler).to receive(:sleep).with(5)
+ expect(sampler).to receive(:sample)
+ expect(sampler).to receive(:loop).and_yield
+
+ sampler.start.join
+ end
+ end
+
+ describe '#sample' do
+ it 'samples various statistics' do
+ expect(sampler).to receive(:sample_memory_usage)
+ expect(sampler).to receive(:sample_file_descriptors)
+ expect(sampler).to receive(:sample_objects)
+ expect(sampler).to receive(:sample_gc)
+ expect(sampler).to receive(:flush)
+
+ sampler.sample
+ end
+
+ it 'clears any GC profiles' do
+ expect(sampler).to receive(:flush)
+ expect(GC::Profiler).to receive(:clear)
+
+ sampler.sample
+ end
+ end
+
+ describe '#flush' do
+ it 'schedules the metrics using Sidekiq' do
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([an_instance_of(Hash)])
+
+ sampler.sample_memory_usage
+ sampler.flush
+ end
+ end
+
+ describe '#sample_memory_usage' do
+ it 'adds a metric containing the memory usage' do
+ expect(Gitlab::Metrics::System).to receive(:memory_usage).
+ and_return(9000)
+
+ expect(sampler).to receive(:add_metric).
+ with(/memory_usage/, value: 9000).
+ and_call_original
+
+ sampler.sample_memory_usage
+ end
+ end
+
+ describe '#sample_file_descriptors' do
+ it 'adds a metric containing the amount of open file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:file_descriptor_count).
+ and_return(4)
+
+ expect(sampler).to receive(:add_metric).
+ with(/file_descriptors/, value: 4).
+ and_call_original
+
+ sampler.sample_file_descriptors
+ 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
+
+ sampler.sample_objects
+ end
+ end
+
+ describe '#sample_gc' do
+ it 'adds a metric containing garbage collection statistics' do
+ expect(GC::Profiler).to receive(:total_time).and_return(0.24)
+
+ expect(sampler).to receive(:add_metric).
+ with(/gc_statistics/, an_instance_of(Hash)).
+ and_call_original
+
+ sampler.sample_gc
+ end
+ end
+
+ describe '#add_metric' do
+ it 'prefixes the series name for a Rails process' do
+ expect(sampler).to receive(:sidekiq?).and_return(false)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('rails_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+
+ it 'prefixes the series name for a Sidekiq process' do
+ expect(sampler).to receive(:sidekiq?).and_return(true)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('sidekiq_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
new file mode 100644
index 00000000000..5882e7d81c7
--- /dev/null
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::SidekiqMiddleware do
+ let(:middleware) { described_class.new }
+
+ describe '#call' do
+ it 'tracks the transaction' do
+ worker = Class.new.new
+
+ expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
+
+ middleware.call(worker, 'test', :test) { nil }
+ end
+ end
+
+ describe '#tag_worker' do
+ it 'adds the worker class and action to the transaction' do
+ trans = Gitlab::Metrics::Transaction.new
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
+
+ expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform')
+
+ middleware.tag_worker(trans, worker)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
new file mode 100644
index 00000000000..05e4fbbeb51
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Subscribers::ActionView do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+
+ let(:subscriber) { described_class.new }
+
+ let(:event) do
+ root = Rails.root.to_s
+
+ double(:event, duration: 2.1,
+ payload: { identifier: "#{root}/app/views/x.html.haml" })
+ end
+
+ before do
+ allow(subscriber).to receive(:current_transaction).and_return(transaction)
+
+ allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
+ and_return(['app/views/x.html.haml', 4])
+ end
+
+ describe '#render_template' do
+ it 'tracks rendering of a template' do
+ values = { duration: 2.1 }
+ tags = {
+ view: 'app/views/x.html.haml',
+ file: 'app/views/x.html.haml',
+ line: 4
+ }
+
+ expect(transaction).to receive(:increment).
+ with(:view_duration, 2.1)
+
+ expect(transaction).to receive(:add_metric).
+ with(described_class::SERIES, values, tags)
+
+ subscriber.render_template(event)
+ 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
new file mode 100644
index 00000000000..7bc070a4d09
--- /dev/null
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Subscribers::ActiveRecord do
+ let(:transaction) { Gitlab::Metrics::Transaction.new }
+ let(:subscriber) { described_class.new }
+
+ let(:event) do
+ double(:event, duration: 0.2,
+ payload: { sql: 'SELECT * FROM users WHERE id = 10' })
+ end
+
+ describe '#sql' do
+ describe 'without a current transaction' do
+ it 'simply returns' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to_not receive(:increment)
+
+ subscriber.sql(event)
+ end
+ end
+
+ describe 'with a current transaction' do
+ it 'increments the :sql_duration value' do
+ expect(subscriber).to receive(:current_transaction).
+ at_least(:once).
+ and_return(transaction)
+
+ expect(transaction).to receive(:increment).
+ with(:sql_duration, 0.2)
+
+ subscriber.sql(event)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
new file mode 100644
index 00000000000..f8c1d956ca1
--- /dev/null
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::System do
+ if File.exist?('/proc')
+ describe '.memory_usage' do
+ it "returns the process' memory usage in bytes" do
+ expect(described_class.memory_usage).to be > 0
+ end
+ end
+
+ describe '.file_descriptor_count' do
+ it 'returns the amount of open file descriptors' do
+ expect(described_class.file_descriptor_count).to be > 0
+ end
+ end
+ else
+ describe '.memory_usage' do
+ it 'returns 0.0' do
+ expect(described_class.memory_usage).to eq(0.0)
+ end
+ end
+
+ describe '.file_descriptor_count' do
+ it 'returns 0' do
+ expect(described_class.file_descriptor_count).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
new file mode 100644
index 00000000000..b9b94947afa
--- /dev/null
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics::Transaction do
+ let(:transaction) { described_class.new }
+
+ describe '#duration' do
+ it 'returns the duration of a transaction in seconds' do
+ transaction.run { sleep(0.5) }
+
+ expect(transaction.duration).to be >= 0.5
+ end
+ end
+
+ describe '#run' do
+ it 'yields the supplied block' do
+ expect { |b| transaction.run(&b) }.to yield_control
+ end
+
+ it 'stores the transaction in the current thread' do
+ transaction.run do
+ expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
+ end
+ end
+
+ it 'removes the transaction from the current thread upon completion' do
+ transaction.run { }
+
+ expect(Thread.current[described_class::THREAD_KEY]).to be_nil
+ end
+ end
+
+ describe '#add_metric' do
+ it 'adds a metric tagged with the transaction UUID' do
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('rails_foo', { number: 10 }, { transaction_id: transaction.uuid })
+
+ transaction.add_metric('foo', number: 10)
+ end
+ end
+
+ describe '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ expect(transaction).to receive(:add_metric).
+ with('transactions', { duration: 0.0, time: 3 }, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#add_tag' do
+ it 'adds a tag' do
+ transaction.add_tag(:foo, 'bar')
+
+ expect(transaction.tags).to eq({ foo: 'bar' })
+ end
+ end
+
+ describe '#finish' do
+ it 'tracks the transaction details and submits them to Sidekiq' do
+ expect(transaction).to receive(:track_self)
+ expect(transaction).to receive(:submit)
+
+ transaction.finish
+ end
+ end
+
+ describe '#track_self' do
+ it 'adds a metric for the transaction itself' do
+ expect(transaction).to receive(:add_metric).
+ with('transactions', { duration: transaction.duration }, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#submit' do
+ it 'submits the metrics to Sidekiq' do
+ transaction.track_self
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([an_instance_of(Hash)])
+
+ transaction.submit
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
new file mode 100644
index 00000000000..c2782f95c8e
--- /dev/null
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::Metrics do
+ describe '.settings' do
+ it 'returns a Hash' do
+ expect(described_class.settings).to be_an_instance_of(Hash)
+ end
+ end
+
+ describe '.enabled?' do
+ it 'returns a boolean' do
+ expect([true, false].include?(described_class.enabled?)).to eq(true)
+ end
+ end
+
+ describe '.last_relative_application_frame' do
+ it 'returns an Array containing a file path and line number' do
+ file, line = described_class.last_relative_application_frame
+
+ expect(line).to eq(__LINE__ - 2)
+ expect(file).to eq('spec/lib/gitlab/metrics_spec.rb')
+ end
+ end
+
+ describe '#submit_metrics' do
+ it 'prepares and writes the metrics to InfluxDB' do
+ connection = double(:connection)
+ pool = double(:pool)
+
+ expect(pool).to receive(:with).and_yield(connection)
+ expect(connection).to receive(:write_points).with(an_instance_of(Array))
+ expect(Gitlab::Metrics).to receive(:pool).and_return(pool)
+
+ described_class.submit_metrics([{ 'series' => 'kittens', 'tags' => {} }])
+ end
+ end
+
+ describe '#prepare_metrics' do
+ it 'returns a Hash with the keys as Symbols' do
+ metrics = described_class.
+ prepare_metrics([{ 'values' => {}, 'tags' => {} }])
+
+ expect(metrics).to eq([{ values: {}, tags: {} }])
+ end
+
+ it 'escapes tag values' do
+ metrics = described_class.prepare_metrics([
+ { 'values' => {}, 'tags' => { 'foo' => 'bar=' } }
+ ])
+
+ expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }])
+ end
+
+ it 'drops empty tags' do
+ metrics = described_class.prepare_metrics([
+ { 'values' => {}, 'tags' => { 'cats' => '', 'dogs' => nil } }
+ ])
+
+ expect(metrics).to eq([{ values: {}, tags: {} }])
+ end
+ end
+
+ describe '#escape_value' do
+ it 'escapes an equals sign' do
+ expect(described_class.escape_value('foo=')).to eq('foo\\=')
+ end
+
+ it 'casts values to Strings' do
+ expect(described_class.escape_value(10)).to eq('10')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb
index 448cd0c6880..6cbdae737f4 100644
--- a/spec/lib/gitlab/note_data_builder_spec.rb
+++ b/spec/lib/gitlab/note_data_builder_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Gitlab::NoteDataBuilder' do
+describe 'Gitlab::NoteDataBuilder', lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::NoteDataBuilder.build(note, user) }
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
index 5632f2306ec..8aaeb5779d3 100644
--- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::OAuth::AuthHash do
+describe Gitlab::OAuth::AuthHash, lib: true do
let(:auth_hash) do
Gitlab::OAuth::AuthHash.new(
OmniAuth::AuthHash.new(
@@ -14,7 +14,7 @@ describe Gitlab::OAuth::AuthHash do
let(:uid_raw) do
"CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net"
end
- let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk@example.net" }
+ let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk_ABC-123@example.net" }
let(:nickname_raw) { "ok\xC3\xBC\xC3\xA7\xC3\xBCk" }
let(:first_name_raw) { 'Onur' }
let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" }
@@ -66,7 +66,7 @@ describe Gitlab::OAuth::AuthHash do
before { info_hash.delete(:nickname) }
it 'takes the first part of the email as username' do
- expect(auth_hash.username).to eql 'onur-kucuk'
+ expect(auth_hash.username).to eql 'onur.kucuk_ABC-123'
end
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index fd3ab1fb7c8..925bc442a90 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::OAuth::User do
+describe Gitlab::OAuth::User, lib: true do
let(:oauth_user) { Gitlab::OAuth::User.new(auth_hash) }
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index e53efec6c67..795cf241278 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Gitlab::Popen', no_db: true do
+describe 'Gitlab::Popen', lib: true, no_db: true do
let(:path) { Rails.root.join('tmp').to_s }
before do
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 32a25f08cac..efc2e5f4ef1 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::ProjectSearchResults do
+describe Gitlab::ProjectSearchResults, lib: true do
let(:project) { create(:project) }
let(:query) { 'hello world' }
@@ -9,7 +9,7 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to be_nil }
- it { expect(results.query).to eq('hello\\ world') }
+ it { expect(results.query).to eq('hello world') }
end
describe 'initialize with ref' do
@@ -18,6 +18,6 @@ describe Gitlab::ProjectSearchResults do
it { expect(results.project).to eq(project) }
it { expect(results.repository_ref).to eq(ref) }
- it { expect(results.query).to eq('hello\\ world') }
+ it { expect(results.query).to eq('hello world') }
end
end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
index 1b8ba7b4d43..3ef61685398 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/push_data_builder_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Gitlab::PushDataBuilder' do
+describe 'Gitlab::PushDataBuilder', lib: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -17,6 +17,9 @@ describe 'Gitlab::PushDataBuilder' do
it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) }
it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) }
it { expect(data[:total_commits_count]).to eq(3) }
+ it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) }
+ it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:commits].first[:removed]).to eq([]) }
end
describe :build do
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index ad84d2274e8..7d963795e17 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::ReferenceExtractor do
+describe Gitlab::ReferenceExtractor, lib: true do
let(:project) { create(:project) }
subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
@@ -97,6 +97,16 @@ describe Gitlab::ReferenceExtractor do
expect(extracted.first.commit_to).to eq commit
end
+ context 'with an external issue tracker' do
+ let(:project) { create(:jira_project) }
+ subject { described_class.new(project, project.creator) }
+
+ 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)]
+ end
+ end
+
context 'with a project with an underscore' do
let(:other_project) { create(:project, path: 'test_project') }
let(:issue) { create(:issue, project: other_project) }
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 7fdc8fa600d..d67ee423b9b 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -1,7 +1,7 @@
# coding: utf-8
require 'spec_helper'
-describe Gitlab::Regex do
+describe Gitlab::Regex, lib: true do
describe 'project path regex' do
it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) }
it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) }
diff --git a/spec/lib/gitlab/sherlock/collection_spec.rb b/spec/lib/gitlab/sherlock/collection_spec.rb
new file mode 100644
index 00000000000..de6bb86c5dd
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/collection_spec.rb
@@ -0,0 +1,82 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Collection, lib: true do
+ let(:collection) { described_class.new }
+
+ let(:transaction) do
+ Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures')
+ end
+
+ describe '#add' do
+ it 'adds a new transaction' do
+ collection.add(transaction)
+
+ expect(collection).to_not be_empty
+ end
+
+ it 'is aliased as <<' do
+ collection << transaction
+
+ expect(collection).to_not be_empty
+ end
+ end
+
+ describe '#each' do
+ it 'iterates over every transaction' do
+ collection.add(transaction)
+
+ expect { |b| collection.each(&b) }.to yield_with_args(transaction)
+ end
+ end
+
+ describe '#clear' do
+ it 'removes all transactions' do
+ collection.add(transaction)
+
+ collection.clear
+
+ expect(collection).to be_empty
+ end
+ end
+
+ describe '#empty?' do
+ it 'returns true for an empty collection' do
+ expect(collection).to be_empty
+ end
+
+ it 'returns false for a collection with a transaction' do
+ collection.add(transaction)
+
+ expect(collection).to_not be_empty
+ end
+ end
+
+ describe '#find_transaction' do
+ it 'returns the transaction for the given ID' do
+ collection.add(transaction)
+
+ expect(collection.find_transaction(transaction.id)).to eq(transaction)
+ end
+
+ it 'returns nil when no transaction could be found' do
+ collection.add(transaction)
+
+ expect(collection.find_transaction('cats')).to be_nil
+ end
+ end
+
+ describe '#newest_first' do
+ it 'returns transactions sorted from new to old' do
+ trans1 = Gitlab::Sherlock::Transaction.new('POST', '/cat_pictures')
+ trans2 = Gitlab::Sherlock::Transaction.new('POST', '/more_cat_pictures')
+
+ allow(trans1).to receive(:finished_at).and_return(Time.utc(2015, 1, 1))
+ allow(trans2).to receive(:finished_at).and_return(Time.utc(2015, 1, 2))
+
+ collection.add(trans1)
+ collection.add(trans2)
+
+ expect(collection.newest_first).to eq([trans2, trans1])
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/file_sample_spec.rb b/spec/lib/gitlab/sherlock/file_sample_spec.rb
new file mode 100644
index 00000000000..cadf8bbce78
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/file_sample_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::FileSample, lib: true do
+ let(:sample) { described_class.new(__FILE__, [], 150.4, 2) }
+
+ describe '#id' do
+ it 'returns the ID' do
+ expect(sample.id).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#file' do
+ it 'returns the file path' do
+ expect(sample.file).to eq(__FILE__)
+ end
+ end
+
+ describe '#line_samples' do
+ it 'returns the line samples' do
+ expect(sample.line_samples).to eq([])
+ end
+ end
+
+ describe '#events' do
+ it 'returns the total number of events' do
+ expect(sample.events).to eq(2)
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the total execution time' do
+ expect(sample.duration).to eq(150.4)
+ end
+ end
+
+ describe '#relative_path' do
+ it 'returns the relative path' do
+ expect(sample.relative_path).
+ to eq('spec/lib/gitlab/sherlock/file_sample_spec.rb')
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the sample ID' do
+ expect(sample.to_param).to eq(sample.id)
+ end
+ end
+
+ describe '#source' do
+ it 'returns the contents of the file' do
+ expect(sample.source).to eq(File.read(__FILE__))
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/line_profiler_spec.rb b/spec/lib/gitlab/sherlock/line_profiler_spec.rb
new file mode 100644
index 00000000000..d57627bba2b
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/line_profiler_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::LineProfiler, lib: true do
+ let(:profiler) { described_class.new }
+
+ describe '#profile' do
+ it 'runs the profiler when using MRI' do
+ allow(profiler).to receive(:mri?).and_return(true)
+ allow(profiler).to receive(:profile_mri)
+
+ profiler.profile { 'cats' }
+ end
+
+ it 'raises NotImplementedError when profiling an unsupported platform' do
+ allow(profiler).to receive(:mri?).and_return(false)
+
+ expect { profiler.profile { 'cats' } }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '#profile_mri' do
+ it 'returns an Array containing the return value and profiling samples' do
+ allow(profiler).to receive(:lineprof).
+ and_yield.
+ and_return({ __FILE__ => [[0, 0, 0, 0]] })
+
+ retval, samples = profiler.profile_mri { 42 }
+
+ expect(retval).to eq(42)
+ expect(samples).to eq([])
+ end
+ end
+
+ describe '#aggregate_rblineprof' do
+ let(:raw_samples) do
+ { __FILE__ => [[30000, 30000, 5, 0], [15000, 15000, 4, 0]] }
+ end
+
+ it 'returns an Array of FileSample objects' do
+ samples = profiler.aggregate_rblineprof(raw_samples)
+
+ expect(samples).to be_an_instance_of(Array)
+ expect(samples[0]).to be_an_instance_of(Gitlab::Sherlock::FileSample)
+ end
+
+ describe 'the first FileSample object' do
+ let(:file_sample) do
+ profiler.aggregate_rblineprof(raw_samples)[0]
+ end
+
+ it 'uses the correct file path' do
+ expect(file_sample.file).to eq(__FILE__)
+ end
+
+ it 'contains a list of line samples' do
+ line_sample = file_sample.line_samples[0]
+
+ expect(line_sample).to be_an_instance_of(Gitlab::Sherlock::LineSample)
+
+ expect(line_sample.duration).to eq(15.0)
+ expect(line_sample.events).to eq(4)
+ end
+
+ it 'contains the total file execution time' do
+ expect(file_sample.duration).to eq(30.0)
+ end
+
+ it 'contains the total amount of file events' do
+ expect(file_sample.events).to eq(5)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/line_sample_spec.rb b/spec/lib/gitlab/sherlock/line_sample_spec.rb
new file mode 100644
index 00000000000..f9b61f8684e
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/line_sample_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::LineSample, lib: true do
+ let(:sample) { described_class.new(150.0, 4) }
+
+ describe '#duration' do
+ it 'returns the duration' do
+ expect(sample.duration).to eq(150.0)
+ end
+ end
+
+ describe '#events' do
+ it 'returns the amount of events' do
+ expect(sample.events).to eq(4)
+ end
+ end
+
+ describe '#percentage_of' do
+ it 'returns the percentage of 1500.0' do
+ expect(sample.percentage_of(1500.0)).to be_within(0.1).of(10.0)
+ end
+ end
+
+ describe '#majority_of' do
+ it 'returns true if the sample takes up the majority of the given duration' do
+ expect(sample.majority_of?(500.0)).to eq(true)
+ end
+
+ it "returns false if the sample doesn't take up the majority of the given duration" do
+ expect(sample.majority_of?(1500.0)).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/location_spec.rb b/spec/lib/gitlab/sherlock/location_spec.rb
new file mode 100644
index 00000000000..5739afa6b1e
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/location_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Location, lib: true do
+ let(:location) { described_class.new(__FILE__, 1) }
+
+ describe 'from_ruby_location' do
+ it 'creates a Location from a Thread::Backtrace::Location' do
+ input = caller_locations[0]
+ output = described_class.from_ruby_location(input)
+
+ expect(output).to be_an_instance_of(described_class)
+ expect(output.path).to eq(input.path)
+ expect(output.line).to eq(input.lineno)
+ end
+ end
+
+ describe '#path' do
+ it 'returns the file path' do
+ expect(location.path).to eq(__FILE__)
+ end
+ end
+
+ describe '#line' do
+ it 'returns the line number' do
+ expect(location.line).to eq(1)
+ end
+ end
+
+ describe '#application?' do
+ it 'returns true for an application frame' do
+ expect(location.application?).to eq(true)
+ end
+
+ it 'returns false for a non application frame' do
+ loc = described_class.new('/tmp/cats.rb', 1)
+
+ expect(loc.application?).to eq(false)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/middleware_spec.rb b/spec/lib/gitlab/sherlock/middleware_spec.rb
new file mode 100644
index 00000000000..2bbeb25ce98
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/middleware_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Middleware, lib: true do
+ let(:app) { double(:app) }
+ let(:middleware) { described_class.new(app) }
+
+ describe '#call' do
+ describe 'when instrumentation is enabled' do
+ it 'instruments a request' do
+ allow(middleware).to receive(:instrument?).and_return(true)
+ allow(middleware).to receive(:call_with_instrumentation)
+
+ middleware.call({})
+ end
+ end
+
+ describe 'when instrumentation is disabled' do
+ it "doesn't instrument a request" do
+ allow(middleware).to receive(:instrument).and_return(false)
+ allow(app).to receive(:call)
+
+ middleware.call({})
+ end
+ end
+ end
+
+ describe '#call_with_instrumentation' do
+ it 'instruments a request' do
+ trans = double(:transaction)
+ retval = 'cats are amazing'
+ env = {}
+
+ allow(app).to receive(:call).with(env).and_return(retval)
+ allow(middleware).to receive(:transaction_from_env).and_return(trans)
+ allow(trans).to receive(:run).and_yield.and_return(retval)
+ allow(Gitlab::Sherlock.collection).to receive(:add).with(trans)
+
+ middleware.call_with_instrumentation(env)
+ end
+ end
+
+ describe '#instrument?' do
+ it 'returns false for a text/css request' do
+ env = { 'HTTP_ACCEPT' => 'text/css', 'REQUEST_URI' => '/' }
+
+ expect(middleware.instrument?(env)).to eq(false)
+ end
+
+ it 'returns false for a request to a Sherlock route' do
+ env = {
+ 'HTTP_ACCEPT' => 'text/html',
+ 'REQUEST_URI' => '/sherlock/transactions'
+ }
+
+ expect(middleware.instrument?(env)).to eq(false)
+ end
+
+ it 'returns true for a request that should be instrumented' do
+ env = {
+ 'HTTP_ACCEPT' => 'text/html',
+ 'REQUEST_URI' => '/cats'
+ }
+
+ expect(middleware.instrument?(env)).to eq(true)
+ end
+ end
+
+ describe '#transaction_from_env' do
+ it 'returns a Transaction' do
+ env = {
+ 'HTTP_ACCEPT' => 'text/html',
+ 'REQUEST_URI' => '/cats'
+ }
+
+ expect(middleware.transaction_from_env(env)).
+ to be_an_instance_of(Gitlab::Sherlock::Transaction)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/query_spec.rb b/spec/lib/gitlab/sherlock/query_spec.rb
new file mode 100644
index 00000000000..05da915ccfd
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/query_spec.rb
@@ -0,0 +1,113 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Query, lib: true do
+ let(:started_at) { Time.utc(2015, 1, 1) }
+ let(:finished_at) { started_at + 5 }
+
+ let(:query) do
+ described_class.new('SELECT COUNT(*) FROM users', started_at, finished_at)
+ end
+
+ describe 'new_with_bindings' do
+ it 'returns a Query' do
+ sql = 'SELECT COUNT(*) FROM users WHERE id = $1'
+ bindings = [[double(:column), 10]]
+
+ query = described_class.
+ new_with_bindings(sql, bindings, started_at, finished_at)
+
+ expect(query.query).to eq('SELECT COUNT(*) FROM users WHERE id = 10;')
+ end
+ end
+
+ describe '#id' do
+ it 'returns a String' do
+ expect(query.id).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#query' do
+ it 'returns the query with a trailing semi-colon' do
+ expect(query.query).to eq('SELECT COUNT(*) FROM users;')
+ end
+ end
+
+ describe '#started_at' do
+ it 'returns the start time' do
+ expect(query.started_at).to eq(started_at)
+ end
+ end
+
+ describe '#finished_at' do
+ it 'returns the completion time' do
+ expect(query.finished_at).to eq(finished_at)
+ end
+ end
+
+ describe '#backtrace' do
+ it 'returns the backtrace' do
+ expect(query.backtrace).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the duration in milliseconds' do
+ expect(query.duration).to be_within(0.1).of(5000.0)
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the query ID' do
+ expect(query.to_param).to eq(query.id)
+ end
+ end
+
+ describe '#formatted_query' do
+ it 'returns a formatted version of the query' do
+ expect(query.formatted_query).to eq(<<-EOF.strip)
+SELECT COUNT(*)
+FROM users;
+ EOF
+ end
+ end
+
+ describe '#last_application_frame' do
+ it 'returns the last application frame' do
+ frame = query.last_application_frame
+
+ expect(frame).to be_an_instance_of(Gitlab::Sherlock::Location)
+ expect(frame.path).to eq(__FILE__)
+ end
+ end
+
+ describe '#application_backtrace' do
+ it 'returns an Array of application frames' do
+ frames = query.application_backtrace
+
+ expect(frames).to be_an_instance_of(Array)
+ expect(frames).to_not be_empty
+
+ frames.each do |frame|
+ expect(frame.path).to start_with(Rails.root.to_s)
+ end
+ end
+ end
+
+ describe '#explain' do
+ it 'returns the query plan as a String' do
+ lines = [
+ ['Aggregate (cost=123 rows=1)'],
+ [' -> Index Only Scan using index_cats_are_amazing']
+ ]
+
+ result = double(:result, values: lines)
+
+ allow(query).to receive(:raw_explain).and_return(result)
+
+ expect(query.explain).to eq(<<-EOF.strip)
+Aggregate (cost=123 rows=1)
+ -> Index Only Scan using index_cats_are_amazing
+ EOF
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sherlock/transaction_spec.rb b/spec/lib/gitlab/sherlock/transaction_spec.rb
new file mode 100644
index 00000000000..7553f2a045f
--- /dev/null
+++ b/spec/lib/gitlab/sherlock/transaction_spec.rb
@@ -0,0 +1,235 @@
+require 'spec_helper'
+
+describe Gitlab::Sherlock::Transaction, lib: true do
+ let(:transaction) { described_class.new('POST', '/cat_pictures') }
+
+ describe '#id' do
+ it 'returns the transaction ID' do
+ expect(transaction.id).to be_an_instance_of(String)
+ end
+ end
+
+ describe '#type' do
+ it 'returns the type' do
+ expect(transaction.type).to eq('POST')
+ end
+ end
+
+ describe '#path' do
+ it 'returns the path' do
+ expect(transaction.path).to eq('/cat_pictures')
+ end
+ end
+
+ describe '#queries' do
+ it 'returns an Array of queries' do
+ expect(transaction.queries).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#file_samples' do
+ it 'returns an Array of file samples' do
+ expect(transaction.file_samples).to be_an_instance_of(Array)
+ end
+ end
+
+ describe '#started_at' do
+ it 'returns the start time' do
+ allow(transaction).to receive(:profile_lines).and_yield
+
+ transaction.run { 'cats are amazing' }
+
+ expect(transaction.started_at).to be_an_instance_of(Time)
+ end
+ end
+
+ describe '#finished_at' do
+ it 'returns the completion time' do
+ allow(transaction).to receive(:profile_lines).and_yield
+
+ transaction.run { 'cats are amazing' }
+
+ expect(transaction.finished_at).to be_an_instance_of(Time)
+ end
+ end
+
+ describe '#view_counts' do
+ it 'returns a Hash' do
+ expect(transaction.view_counts).to be_an_instance_of(Hash)
+ end
+
+ it 'sets the default value of a key to 0' do
+ expect(transaction.view_counts['cats.rb']).to be_zero
+ end
+ end
+
+ describe '#run' do
+ it 'runs the transaction' do
+ allow(transaction).to receive(:profile_lines).and_yield
+
+ retval = transaction.run { 'cats are amazing' }
+
+ expect(retval).to eq('cats are amazing')
+ end
+ end
+
+ describe '#duration' do
+ it 'returns the duration in seconds' do
+ start_time = Time.now
+
+ allow(transaction).to receive(:started_at).and_return(start_time)
+ allow(transaction).to receive(:finished_at).and_return(start_time + 5)
+
+ expect(transaction.duration).to be_within(0.1).of(5.0)
+ end
+ end
+
+ describe '#query_duration' do
+ it 'returns the total query duration in seconds' do
+ time = Time.now
+ query1 = Gitlab::Sherlock::Query.new('SELECT 1', time, time + 5)
+ query2 = Gitlab::Sherlock::Query.new('SELECT 2', time, time + 2)
+
+ transaction.queries << query1
+ transaction.queries << query2
+
+ expect(transaction.query_duration).to be_within(0.1).of(7.0)
+ end
+ end
+
+ describe '#to_param' do
+ it 'returns the transaction ID' do
+ expect(transaction.to_param).to eq(transaction.id)
+ end
+ end
+
+ describe '#sorted_queries' do
+ it 'returns the queries in descending order' do
+ start_time = Time.now
+
+ query1 = Gitlab::Sherlock::Query.new('SELECT 1', start_time, start_time)
+
+ query2 = Gitlab::Sherlock::Query.
+ new('SELECT 2', start_time, start_time + 5)
+
+ transaction.queries << query1
+ transaction.queries << query2
+
+ expect(transaction.sorted_queries).to eq([query2, query1])
+ end
+ end
+
+ describe '#sorted_file_samples' do
+ it 'returns the file samples in descending order' do
+ sample1 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1)
+ sample2 = Gitlab::Sherlock::FileSample.new(__FILE__, [], 15.0, 1)
+
+ transaction.file_samples << sample1
+ transaction.file_samples << sample2
+
+ expect(transaction.sorted_file_samples).to eq([sample2, sample1])
+ end
+ end
+
+ describe '#find_query' do
+ it 'returns a Query when found' do
+ query = Gitlab::Sherlock::Query.new('SELECT 1', Time.now, Time.now)
+
+ transaction.queries << query
+
+ expect(transaction.find_query(query.id)).to eq(query)
+ end
+
+ it 'returns nil when no query could be found' do
+ expect(transaction.find_query('cats')).to be_nil
+ end
+ end
+
+ describe '#find_file_sample' do
+ it 'returns a FileSample when found' do
+ sample = Gitlab::Sherlock::FileSample.new(__FILE__, [], 10.0, 1)
+
+ transaction.file_samples << sample
+
+ expect(transaction.find_file_sample(sample.id)).to eq(sample)
+ end
+
+ it 'returns nil when no file sample could be found' do
+ expect(transaction.find_file_sample('cats')).to be_nil
+ end
+ end
+
+ describe '#profile_lines' do
+ describe 'when line profiling is enabled' do
+ it 'yields the block using the line profiler' do
+ allow(Gitlab::Sherlock).to receive(:enable_line_profiler?).
+ and_return(true)
+
+ allow_any_instance_of(Gitlab::Sherlock::LineProfiler).
+ to receive(:profile).and_return('cats are amazing', [])
+
+ retval = transaction.profile_lines { 'cats are amazing' }
+
+ expect(retval).to eq('cats are amazing')
+ end
+ end
+
+ describe 'when line profiling is disabled' do
+ it 'yields the block' do
+ allow(Gitlab::Sherlock).to receive(:enable_line_profiler?).
+ and_return(false)
+
+ retval = transaction.profile_lines { 'cats are amazing' }
+
+ expect(retval).to eq('cats are amazing')
+ end
+ end
+ end
+
+ describe '#subscribe_to_active_record' do
+ let(:subscription) { transaction.subscribe_to_active_record }
+ let(:time) { Time.now }
+ let(:query_data) { { sql: 'SELECT 1', binds: [] } }
+
+ after do
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ it 'tracks executed queries' do
+ expect(transaction).to receive(:track_query).
+ with('SELECT 1', [], time, time)
+
+ subscription.publish('test', time, time, nil, query_data)
+ end
+
+ it 'only tracks queries triggered from the transaction thread' do
+ expect(transaction).to_not receive(:track_query)
+
+ Thread.new { subscription.publish('test', time, time, nil, query_data) }.
+ join
+ end
+ end
+
+ describe '#subscribe_to_action_view' do
+ let(:subscription) { transaction.subscribe_to_action_view }
+ let(:time) { Time.now }
+ let(:view_data) { { identifier: 'foo.rb' } }
+
+ after do
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ it 'tracks rendered views' do
+ expect(transaction).to receive(:track_view).with('foo.rb')
+
+ subscription.publish('test', time, time, nil, view_data)
+ end
+
+ it 'only tracks views rendered from the transaction thread' do
+ expect(transaction).to_not receive(:track_view)
+
+ Thread.new { subscription.publish('test', time, time, nil, view_data) }.
+ join
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
new file mode 100644
index 00000000000..0cdbab87544
--- /dev/null
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::SQL::Union, lib: true do
+ describe '#to_sql' do
+ it 'returns a String joining relations together using a UNION' do
+ rel1 = User.where(email: 'alice@example.com')
+ rel2 = User.where(email: 'bob@example.com')
+ union = described_class.new([rel1, rel2])
+
+ sql1 = rel1.reorder(nil).to_sql
+ sql2 = rel2.reorder(nil).to_sql
+
+ expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
index e554458e41c..7a140518dd2 100644
--- a/spec/lib/gitlab/themes_spec.rb
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Themes do
+describe Gitlab::Themes, lib: true do
describe '.body_classes' do
it 'returns a space-separated list of class names' do
css = described_class.body_classes
diff --git a/spec/lib/gitlab/upgrader_spec.rb b/spec/lib/gitlab/upgrader_spec.rb
index 8df84665e16..e958e087a80 100644
--- a/spec/lib/gitlab/upgrader_spec.rb
+++ b/spec/lib/gitlab/upgrader_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Upgrader do
+describe Gitlab::Upgrader, lib: true do
let(:upgrader) { Gitlab::Upgrader.new }
let(:current_version) { Gitlab::VERSION }
diff --git a/spec/lib/gitlab/uploads_transfer_spec.rb b/spec/lib/gitlab/uploads_transfer_spec.rb
index 260364a513e..4092f7fb638 100644
--- a/spec/lib/gitlab/uploads_transfer_spec.rb
+++ b/spec/lib/gitlab/uploads_transfer_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::UploadsTransfer do
+describe Gitlab::UploadsTransfer, lib: true do
before do
@root_dir = File.join(Rails.root, "public", "uploads")
@upload_transfer = Gitlab::UploadsTransfer.new
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 5153ed15af3..f023be6ae45 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::UrlBuilder do
+describe Gitlab::UrlBuilder, lib: true do
describe 'When asking for an issue' do
it 'returns the issue url' do
issue = create(:issue)
diff --git a/spec/lib/gitlab/version_info_spec.rb b/spec/lib/gitlab/version_info_spec.rb
index 18f71b40fe0..706ee9bec58 100644
--- a/spec/lib/gitlab/version_info_spec.rb
+++ b/spec/lib/gitlab/version_info_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Gitlab::VersionInfo', no_db: true do
+describe 'Gitlab::VersionInfo', lib: true, no_db: true do
before do
@unknown = Gitlab::VersionInfo.new
@v0_0_1 = Gitlab::VersionInfo.new(0, 0, 1)
diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb
index 37240d51310..63b5292b098 100644
--- a/spec/lib/repository_cache_spec.rb
+++ b/spec/lib/repository_cache_spec.rb
@@ -1,6 +1,6 @@
require_relative '../../lib/repository_cache'
-describe RepositoryCache do
+describe RepositoryCache, lib: true do
let(:backend) { double('backend').as_null_object }
let(:cache) { RepositoryCache.new('example', backend) }
diff --git a/spec/lib/votes_spec.rb b/spec/lib/votes_spec.rb
deleted file mode 100644
index 39e5d054e62..00000000000
--- a/spec/lib/votes_spec.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require 'spec_helper'
-
-describe Issue, 'Votes' do
- let(:issue) { create(:issue) }
-
- describe "#upvotes" do
- it "with no notes has a 0/0 score" do
- expect(issue.upvotes).to eq(0)
- end
-
- it "should recognize non-+1 notes" do
- add_note "No +1 here"
- expect(issue.notes.size).to eq(1)
- expect(issue.notes.first.upvote?).to be_falsey
- expect(issue.upvotes).to eq(0)
- end
-
- it "should recognize a single +1 note" do
- add_note "+1 This is awesome"
- expect(issue.upvotes).to eq(1)
- end
-
- it 'should recognize multiple +1 notes' do
- add_note '+1 This is awesome', create(:user)
- add_note '+1 I want this', create(:user)
- expect(issue.upvotes).to eq(2)
- end
-
- it 'should not count 2 +1 votes from the same user' do
- add_note '+1 This is awesome'
- add_note '+1 I want this'
- expect(issue.upvotes).to eq(1)
- end
- end
-
- describe "#downvotes" do
- it "with no notes has a 0/0 score" do
- expect(issue.downvotes).to eq(0)
- end
-
- it "should recognize non--1 notes" do
- add_note "Almost got a -1"
- expect(issue.notes.size).to eq(1)
- expect(issue.notes.first.downvote?).to be_falsey
- expect(issue.downvotes).to eq(0)
- end
-
- it "should recognize a single -1 note" do
- add_note "-1 This is bad"
- expect(issue.downvotes).to eq(1)
- end
-
- it "should recognize multiple -1 notes" do
- add_note('-1 This is bad', create(:user))
- add_note('-1 Away with this', create(:user))
- expect(issue.downvotes).to eq(2)
- end
- end
-
- describe "#votes_count" do
- it "with no notes has a 0/0 score" do
- expect(issue.votes_count).to eq(0)
- end
-
- it "should recognize non notes" do
- add_note "No +1 here"
- expect(issue.notes.size).to eq(1)
- expect(issue.votes_count).to eq(0)
- end
-
- it "should recognize a single +1 note" do
- add_note "+1 This is awesome"
- expect(issue.votes_count).to eq(1)
- end
-
- it "should recognize a single -1 note" do
- add_note "-1 This is bad"
- expect(issue.votes_count).to eq(1)
- end
-
- it "should recognize multiple notes" do
- add_note('+1 This is awesome', create(:user))
- add_note('-1 This is bad', create(:user))
- add_note('+1 I want this', create(:user))
- expect(issue.votes_count).to eq(3)
- end
-
- it 'should not count 2 -1 votes from the same user' do
- add_note '-1 This is suspicious'
- add_note '-1 This is bad'
- expect(issue.votes_count).to eq(1)
- end
- end
-
- describe "#upvotes_in_percent" do
- it "with no notes has a 0% score" do
- expect(issue.upvotes_in_percent).to eq(0)
- end
-
- it "should count a single 1 note as 100%" do
- add_note "+1 This is awesome"
- expect(issue.upvotes_in_percent).to eq(100)
- end
-
- it 'should count multiple +1 notes as 100%' do
- add_note('+1 This is awesome', create(:user))
- add_note('+1 I want this', create(:user))
- expect(issue.upvotes_in_percent).to eq(100)
- end
-
- it 'should count fractions for multiple +1 and -1 notes correctly' do
- add_note('+1 This is awesome', create(:user))
- add_note('+1 I want this', create(:user))
- add_note('-1 This is bad', create(:user))
- add_note('+1 me too', create(:user))
- expect(issue.upvotes_in_percent).to eq(75)
- end
- end
-
- describe "#downvotes_in_percent" do
- it "with no notes has a 0% score" do
- expect(issue.downvotes_in_percent).to eq(0)
- end
-
- it "should count a single -1 note as 100%" do
- add_note "-1 This is bad"
- expect(issue.downvotes_in_percent).to eq(100)
- end
-
- it 'should count multiple -1 notes as 100%' do
- add_note('-1 This is bad', create(:user))
- add_note('-1 Away with this', create(:user))
- expect(issue.downvotes_in_percent).to eq(100)
- end
-
- it 'should count fractions for multiple +1 and -1 notes correctly' do
- add_note('+1 This is awesome', create(:user))
- add_note('+1 I want this', create(:user))
- add_note('-1 This is bad', create(:user))
- add_note('+1 me too', create(:user))
- expect(issue.downvotes_in_percent).to eq(25)
- end
- end
-
- describe '#filter_superceded_votes' do
-
- it 'should count a users vote only once amongst multiple votes' do
- add_note('-1 This needs work before I will accept it')
- add_note('+1 I want this', create(:user))
- add_note('+1 This is is awesome', create(:user))
- add_note('+1 this looks good now')
- add_note('+1 This is awesome', create(:user))
- add_note('+1 me too', create(:user))
- expect(issue.downvotes).to eq(0)
- expect(issue.upvotes).to eq(5)
- end
-
- it 'should count each users vote only once' do
- add_note '-1 This needs work before it will be accepted'
- add_note '+1 I like this'
- add_note '+1 I still like this'
- add_note '+1 I really like this'
- add_note '+1 Give me this now!!!!'
- expect(issue.downvotes).to eq(0)
- expect(issue.upvotes).to eq(1)
- end
-
- it 'should count a users vote only once without caring about comments' do
- add_note '-1 This needs work before it will be accepted'
- add_note 'Comment 1'
- add_note 'Another comment'
- add_note '+1 vote'
- add_note 'final comment'
- expect(issue.downvotes).to eq(0)
- expect(issue.upvotes).to eq(1)
- end
-
- end
-
- def add_note(text, author = issue.author)
- created_at = Time.now - 1.hour + Note.count.seconds
- issue.notes << create(:note,
- note: text,
- project: issue.project,
- author_id: author.id,
- created_at: created_at)
- end
-end
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
new file mode 100644
index 00000000000..eb433c38873
--- /dev/null
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe AbuseReportMailer do
+ include EmailSpec::Matchers
+
+ describe '.notify' do
+ context 'with admin_notification_email set' do
+ before do
+ stub_application_setting(admin_notification_email: 'admin@example.com')
+ end
+
+ it 'sends to the admin_notification_email' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to deliver_to 'admin@example.com'
+ end
+
+ it 'includes the user in the subject' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse"
+ end
+ end
+
+ context 'with no admin_notification_email set' do
+ it 'returns early' do
+ stub_application_setting(admin_notification_email: nil)
+
+ expect { described_class.notify(spy).deliver_now }.
+ not_to change { ActionMailer::Base.deliveries.count }
+ end
+ end
+ end
+end
diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb
deleted file mode 100644
index b83fb41603b..00000000000
--- a/spec/mailers/ci/notify_spec.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'spec_helper'
-
-describe Ci::Notify do
- include EmailSpec::Helpers
- include EmailSpec::Matchers
-
- before do
- @commit = FactoryGirl.create :ci_commit
- @build = FactoryGirl.create :ci_build, commit: @commit
- end
-
- describe 'build success' do
- subject { Ci::Notify.build_success_email(@build.id, 'wow@example.com') }
-
- it 'has the correct subject' do
- should have_subject /Build success for/
- end
-
- it 'contains name of project' do
- should have_body_text /build successful/
- end
- end
-
- describe 'build fail' do
- subject { Ci::Notify.build_fail_email(@build.id, 'wow@example.com') }
-
- it 'has the correct subject' do
- should have_subject /Build failed for/
- end
-
- it 'contains name of project' do
- should have_body_text /build failed/
- end
- end
-end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index cb67ec95d57..154901a2fbc 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -13,6 +13,7 @@ describe Notify do
let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
let(:recipient) { create(:user, email: 'recipient@example.com') }
let(:project) { create(:project) }
+ let(:build) { create(:ci_build) }
before(:each) do
ActionMailer::Base.deliveries.clear
@@ -77,6 +78,32 @@ describe Notify do
end
end
+ shared_examples 'it should have Gmail Actions links' do
+ it { is_expected.to have_body_text /ViewAction/ }
+ end
+
+ shared_examples 'it should not have Gmail Actions links' do
+ it { is_expected.to_not have_body_text /ViewAction/ }
+ end
+
+ shared_examples 'it should show Gmail Actions View Issue link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Issue/ }
+ end
+
+ shared_examples 'it should show Gmail Actions View Merge request link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Merge request/ }
+ end
+
+ shared_examples 'it should show Gmail Actions View Commit link' do
+ it_behaves_like 'it should have Gmail Actions links'
+
+ it { is_expected.to have_body_text /View Commit/ }
+ end
+
describe 'for new users, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) }
@@ -87,6 +114,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
+ it_behaves_like 'it should not have Gmail Actions links'
it 'contains the password text' do
is_expected.to have_body_text /Click here to set your password/
@@ -115,6 +143,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'a new user email', new_user_address
+ it_behaves_like 'it should not have Gmail Actions links'
it 'should not contain the new user\'s password' do
is_expected.not_to have_body_text /password/
@@ -127,6 +156,7 @@ describe Notify do
subject { Notify.new_ssh_key_email(key.id) }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'is sent to the new user' do
is_expected.to deliver_to key.user.email
@@ -150,6 +180,8 @@ describe Notify do
subject { Notify.new_email_email(email.id) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent to the new user' do
is_expected.to deliver_to email.user.email
end
@@ -194,6 +226,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
@@ -207,16 +240,19 @@ describe Notify do
describe 'that are new with a description' do
subject { Notify.new_issue_email(issue_with_description.assignee_id, issue_with_description.id) }
+ it_behaves_like 'it should show Gmail Actions View Issue link'
+
it 'contains the description' do
is_expected.to have_body_text /#{issue_with_description.description}/
end
end
describe 'that have been reassigned' do
- subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user) }
+ subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user.id) }
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -243,9 +279,10 @@ describe Notify do
describe 'status changed' do
let(:status) { 'closed' }
- subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user) }
+ subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -269,7 +306,6 @@ describe Notify do
is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/
end
end
-
end
context 'for merge requests' do
@@ -282,6 +318,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -307,6 +344,8 @@ describe Notify do
describe 'that are new with a description' do
subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
+
it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/
end
@@ -317,6 +356,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -343,9 +383,10 @@ describe Notify do
describe 'status changed' do
let(:status) { 'reopened' }
- subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user) }
+ subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) }
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -375,6 +416,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -403,6 +445,7 @@ describe Notify do
subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'has the correct subject' do
is_expected.to have_subject /Project was moved/
@@ -424,13 +467,16 @@ describe Notify do
subject { Notify.project_access_granted_email(project_member.id) }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/
end
+
it 'contains name of project' do
is_expected.to have_body_text /#{project.name}/
end
+
it 'contains new user role' do
is_expected.to have_body_text /#{project_member.human_access}/
end
@@ -445,6 +491,8 @@ describe Notify do
end
shared_examples 'a note email' do
+ it_behaves_like 'it should have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(note_author.name)
@@ -468,7 +516,8 @@ describe Notify do
subject { Notify.note_commit_email(recipient.id, note.id) }
it_behaves_like 'a note email'
- it_behaves_like 'an answer to an existing thread', 'commits'
+ it_behaves_like 'an answer to an existing thread', 'commit'
+ it_behaves_like 'it should show Gmail Actions View Commit link'
it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
@@ -488,6 +537,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
+ it_behaves_like 'it should show Gmail Actions View Merge request link'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -507,6 +557,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue'
+ it_behaves_like 'it should show Gmail Actions View Issue link'
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
@@ -527,6 +578,7 @@ describe Notify do
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 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/
@@ -546,8 +598,10 @@ describe Notify do
let(:user) { create(:user, email: 'old-email@mail.com') }
before do
- user.email = "new-email@mail.com"
- user.save
+ perform_enqueued_jobs do
+ user.email = "new-email@mail.com"
+ user.save
+ end
end
subject { ActionMailer::Base.deliveries.last }
@@ -574,6 +628,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -600,6 +656,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -625,6 +683,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -646,6 +706,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -671,6 +733,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
+ it_behaves_like 'it should not have Gmail Actions links'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -774,6 +838,8 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
+ it_behaves_like 'it should show Gmail Actions View Commit link'
+
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
expect(sender.display_name).to eq(user.name)
@@ -800,4 +866,32 @@ describe Notify do
is_expected.to have_body_text /#{diff_path}/
end
end
+
+ describe 'build success' do
+ before { build.success }
+
+ subject { Notify.build_success_email(build.id, 'wow@example.com') }
+
+ it 'has the correct subject' do
+ should have_subject /Build success for/
+ end
+
+ it 'contains name of project' do
+ should have_body_text build.project_name
+ end
+ end
+
+ describe 'build fail' do
+ before { build.drop }
+
+ subject { Notify.build_fail_email(build.id, 'wow@example.com') }
+
+ it 'has the correct subject' do
+ should have_subject /Build failed for/
+ end
+
+ it 'contains name of project' do
+ should have_body_text build.project_name
+ end
+ end
end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index d45319b25d4..46cab1644c7 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -28,4 +28,21 @@ RSpec.describe AbuseReport, type: :model do
it { is_expected.to validate_presence_of(:message) }
it { is_expected.to validate_uniqueness_of(:user_id) }
end
+
+ describe '#notify' do
+ it 'delivers' do
+ expect(AbuseReportMailer).to receive(:notify).with(subject.id).
+ and_return(spy)
+
+ subject.notify
+ end
+
+ it 'returns early when not persisted' do
+ report = build(:abuse_report)
+
+ expect(AbuseReportMailer).not_to receive(:notify)
+
+ report.notify
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index de0b2ef4cda..35d8220ae54 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -23,16 +23,37 @@
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
# import_sources :text
+# help_page_text :text
+# admin_notification_email :string(255)
+# shared_runners_enabled :boolean default(TRUE), not null
+# max_artifacts_size :integer default(100), not null
+# runners_registration_token :string(255)
#
require 'spec_helper'
describe ApplicationSetting, models: true do
- it { expect(ApplicationSetting.create_from_defaults).to be_valid }
+ let(:setting) { ApplicationSetting.create_from_defaults }
- context 'restricted signup domains' do
- let(:setting) { ApplicationSetting.create_from_defaults }
+ it { expect(setting).to be_valid }
+
+ describe 'validations' do
+ let(:http) { 'http://example.com' }
+ let(:https) { 'https://example.com' }
+ let(:ftp) { 'ftp://example.com' }
+
+ it { is_expected.to allow_value(nil).for(:home_page_url) }
+ it { is_expected.to allow_value(http).for(:home_page_url) }
+ it { is_expected.to allow_value(https).for(:home_page_url) }
+ it { is_expected.not_to allow_value(ftp).for(:home_page_url) }
+ it { is_expected.to allow_value(nil).for(:after_sign_out_path) }
+ it { is_expected.to allow_value(http).for(:after_sign_out_path) }
+ it { is_expected.to allow_value(https).for(:after_sign_out_path) }
+ it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) }
+ end
+
+ context 'restricted signup domains' do
it 'set single domain' do
setting.restricted_signup_domains_raw = 'example.com'
expect(setting.restricted_signup_domains).to eq(['example.com'])
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index d80748f23a4..e4cac105110 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -15,11 +15,26 @@
require 'spec_helper'
-describe BroadcastMessage do
+describe BroadcastMessage, models: true do
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
+ describe 'validations' do
+ let(:triplet) { '#000' }
+ let(:hex) { '#AABBCC' }
+
+ it { is_expected.to allow_value(nil).for(:color) }
+ it { is_expected.to allow_value(triplet).for(:color) }
+ it { is_expected.to allow_value(hex).for(:color) }
+ it { is_expected.not_to allow_value('000').for(:color) }
+
+ it { is_expected.to allow_value(nil).for(:font) }
+ it { is_expected.to allow_value(triplet).for(:font) }
+ it { is_expected.to allow_value(hex).for(:font) }
+ it { is_expected.not_to allow_value('000').for(:font) }
+ end
+
describe :current do
it "should return last message if time match" do
broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow)
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 7f5abb83ac2..1c22e3cb7c4 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -25,10 +25,9 @@
require 'spec_helper'
-describe Ci::Build do
- let(:project) { FactoryGirl.create :ci_project }
- let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
- let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+describe Ci::Build, models: true do
+ let(:project) { FactoryGirl.create :empty_project }
+ let(:commit) { FactoryGirl.create :ci_commit, project: project }
let(:build) { FactoryGirl.create :ci_build, commit: commit }
it { is_expected.to validate_presence_of :ref }
@@ -112,7 +111,7 @@ describe Ci::Build do
let(:token) { 'my_secret_token' }
before do
- build.project.update_attributes(token: token)
+ build.project.update_attributes(runners_token: token)
build.update_attributes(trace: token)
end
@@ -120,11 +119,12 @@ describe Ci::Build do
end
end
- describe :timeout do
- subject { build.timeout }
-
- it { is_expected.to eq(commit.project.timeout) }
- end
+ # TODO: build timeout
+ # describe :timeout do
+ # subject { build.timeout }
+ #
+ # it { is_expected.to eq(commit.project.timeout) }
+ # end
describe :options do
let(:options) do
@@ -140,11 +140,12 @@ describe Ci::Build do
it { is_expected.to eq(options) }
end
- describe :allow_git_fetch do
- subject { build.allow_git_fetch }
-
- it { is_expected.to eq(project.allow_git_fetch) }
- end
+ # TODO: allow_git_fetch
+ # describe :allow_git_fetch do
+ # subject { build.allow_git_fetch }
+ #
+ # it { is_expected.to eq(project.allow_git_fetch) }
+ # end
describe :project do
subject { build.project }
@@ -164,12 +165,6 @@ describe Ci::Build do
it { is_expected.to eq(project.name) }
end
- describe :repo_url do
- subject { build.repo_url }
-
- it { is_expected.to eq(project.repo_url_with_auth) }
- end
-
describe :extract_coverage do
context 'valid content & regex' do
subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
@@ -194,6 +189,12 @@ describe Ci::Build do
it { is_expected.to eq(98.29) }
end
+
+ context 'using a regex capture' do
+ subject { build.extract_coverage('TOTAL 9926 3489 65%', 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)') }
+
+ it { is_expected.to eq(65) }
+ end
end
describe :variables do
@@ -266,40 +267,6 @@ describe Ci::Build do
end
end
- describe :project_recipients do
- let(:pusher_email) { 'pusher@gitlab.test' }
- let(:user) { User.new(notification_email: pusher_email) }
- subject { build.project_recipients }
-
- before do
- build.update_attributes(user: user)
- end
-
- it 'should return pusher_email as only recipient when no additional recipients are given' do
- project.update_attributes(email_add_pusher: true,
- email_recipients: '')
- is_expected.to eq([pusher_email])
- end
-
- it 'should return pusher_email and additional recipients' do
- project.update_attributes(email_add_pusher: true,
- email_recipients: 'rec1 rec2')
- is_expected.to eq(['rec1', 'rec2', pusher_email])
- end
-
- it 'should return recipients' do
- project.update_attributes(email_add_pusher: false,
- email_recipients: 'rec1 rec2')
- is_expected.to eq(['rec1', 'rec2'])
- end
-
- it 'should return unique recipients only' do
- project.update_attributes(email_add_pusher: true,
- email_recipients: "rec1 rec1 #{pusher_email}")
- is_expected.to eq(['rec1', pusher_email])
- end
- end
-
describe :can_be_served? do
let(:runner) { FactoryGirl.create :ci_specific_runner }
@@ -400,4 +367,97 @@ describe Ci::Build do
end
end
end
+
+ describe :download_url do
+ subject { build.download_url }
+
+ it "should be nil if artifact doesn't exist" do
+ build.update_attributes(artifacts_file: nil)
+ is_expected.to be_nil
+ end
+
+ it 'should be nil if artifact exist' do
+ gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ build.update_attributes(artifacts_file: gif)
+ is_expected.to_not be_nil
+ end
+ end
+
+ describe :repo_url do
+ let(:build) { FactoryGirl.create :ci_build }
+ let(:project) { build.project }
+
+ subject { build.repo_url }
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.to end_with(".git") }
+ it { is_expected.to start_with(project.web_url[0..6]) }
+ it { is_expected.to include(build.token) }
+ it { is_expected.to include('gitlab-ci-token') }
+ it { is_expected.to include(project.web_url[7..-1]) }
+ 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)
+ end
+
+ describe :merge_request do
+ context 'when a MR has a reference to the commit' do
+ before do
+ @merge_request = create_mr(build, commit, factory: :merge_request)
+
+ commits = [double(id: commit.sha)]
+ allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
+ end
+
+ it 'returns the single associated MR' do
+ expect(build.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ context 'when there is not a MR referencing the commit' 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
+ before do
+ @merge_request = create_mr(build, commit, factory: :merge_request)
+ @merge_request.close!
+ @merge_request2 = create_mr(build, commit, factory: :merge_request)
+
+ commits = [double(id: commit.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])
+ end
+
+ it 'returns the first MR' do
+ expect(build.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ 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
+
+ commits = [double(id: commit.sha), double(id: commit2.sha)]
+ allow(@merge_request).to receive(:commits).and_return(commits)
+ allow(MergeRequest).to receive_message_chain(:includes, :where, :reorder).and_return([@merge_request])
+ end
+
+ it 'returns the current MR' do
+ expect(@build2.merge_request.id).to eq(@merge_request.id)
+ end
+ end
+
+ end
end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index 44dbd083f06..b193e16e7f8 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -1,28 +1,28 @@
# == Schema Information
#
-# Table name: commits
+# Table name: ci_commits
#
-# id :integer not null, primary key
-# project_id :integer
-# ref :string(255)
-# sha :string(255)
-# before_sha :string(255)
-# push_data :text
-# created_at :datetime
-# updated_at :datetime
-# tag :boolean default(FALSE)
-# yaml_errors :text
-# committed_at :datetime
+# id :integer not null, primary key
+# project_id :integer
+# ref :string(255)
+# sha :string(255)
+# before_sha :string(255)
+# push_data :text
+# created_at :datetime
+# updated_at :datetime
+# tag :boolean default(FALSE)
+# yaml_errors :text
+# committed_at :datetime
+# project_id :integer
#
require 'spec_helper'
-describe Ci::Commit do
- let(:project) { FactoryGirl.create :ci_project }
- let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
- let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+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(:gl_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) }
@@ -36,16 +36,16 @@ describe Ci::Commit do
let(:project) { FactoryGirl.create :empty_project }
it 'returns ordered list of commits' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
expect(project.ci_commits.ordered).to eq([commit2, commit1])
end
it 'returns commits ordered by committed_at and id, with nulls last' do
- commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
- commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
- commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
- commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
+ commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project
+ commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project
expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1])
end
end
@@ -161,7 +161,7 @@ describe Ci::Commit do
end
describe :create_builds do
- let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ let!(:commit) { FactoryGirl.create :ci_commit, project: project }
def create_builds(trigger_request = nil)
commit.create_builds('master', false, nil, trigger_request)
@@ -389,9 +389,8 @@ describe Ci::Commit do
end
describe "coverage" do
- let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" }
- let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
- let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ 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
diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb
deleted file mode 100644
index e23d6ae2c28..00000000000
--- a/spec/models/ci/project_services/hip_chat_message_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe Ci::HipChatMessage do
- subject { Ci::HipChatMessage.new(build) }
-
- let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
-
- let(:build) do
- commit.builds.first
- end
-
- context 'when all matrix builds succeed' do
- it 'returns a successful message' do
- commit.create_builds('master', false, nil)
- commit.builds.update_all(status: "success")
- commit.reload
-
- expect(subject.status_color).to eq 'green'
- expect(subject.notify?).to be_falsey
- expect(subject.to_s).to match(/Commit #\d+/)
- expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./)
- end
- end
-
- context 'when at least one matrix build fails' do
- it 'returns a failure message' do
- commit.create_builds('master', false, nil)
- first_build = commit.builds.first
- second_build = commit.builds.last
- first_build.update(status: "success")
- second_build.update(status: "failed")
-
- expect(subject.status_color).to eq 'red'
- expect(subject.notify?).to be_truthy
- expect(subject.to_s).to match(/Commit #\d+/)
- expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./)
- end
- end
-end
diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb
deleted file mode 100644
index d9ccc855edf..00000000000
--- a/spec/models/ci/project_services/hip_chat_service_spec.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-
-require 'spec_helper'
-
-describe Ci::HipChatService do
-
- describe "Validations" do
-
- context "active" do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of :hipchat_room }
- it { is_expected.to validate_presence_of :hipchat_token }
-
- end
- end
-
- describe "Execute" do
-
- let(:service) { Ci::HipChatService.new }
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
- let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' }
-
- before do
- allow(service).to receive_messages(
- project: commit.project,
- project_id: commit.project_id,
- notify_only_broken_builds: false,
- hipchat_room: 123,
- hipchat_token: 'a1b2c3d4e5f6'
- )
-
- WebMock.stub_request(:post, api_url)
- end
-
-
- it "should call the HipChat API" do
- service.execute(build)
- Ci::HipChatNotifierWorker.drain
-
- expect( WebMock ).to have_requested(:post, api_url).once
- end
-
- it "calls the worker with expected arguments" do
- expect( Ci::HipChatNotifierWorker ).to receive(:perform_async) \
- .with(an_instance_of(String), hash_including(
- token: 'a1b2c3d4e5f6',
- room: 123,
- server: 'https://api.hipchat.com',
- color: 'red',
- notify: true
- ))
-
- service.execute(build)
- end
- end
-end
diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb
deleted file mode 100644
index d9b3d34ff15..00000000000
--- a/spec/models/ci/project_services/mail_service_spec.rb
+++ /dev/null
@@ -1,191 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-require 'spec_helper'
-
-describe Ci::MailService do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- end
-
- describe "Validations" do
- context "active" do
- before do
- subject.active = true
- end
- end
- end
-
- describe 'Sends email for' do
- let(:mail) { Ci::MailService.new }
- let(:user) { User.new(notification_email: 'git@example.com')}
-
- describe 'failed build' do
- let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) }
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
- let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
-
- before do
- allow(mail).to receive_messages(
- project: project
- )
- end
-
- it do
- should_email("git@example.com")
- mail.execute(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).to receive(:build_fail_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
- end
- end
-
- describe 'successfull build' do
- let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) }
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
- let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
-
- before do
- allow(mail).to receive_messages(
- project: project
- )
- end
-
- it do
- should_email("git@example.com")
- mail.execute(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
- end
- end
-
- describe 'successfull build and project has email_recipients' do
- let(:project) do
- FactoryGirl.create(:ci_project,
- email_add_pusher: true,
- email_only_broken_builds: false,
- email_recipients: "jeroen@example.com")
- end
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
- let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
-
- before do
- allow(mail).to receive_messages(
- project: project
- )
- end
-
- it do
- should_email("git@example.com")
- should_email("jeroen@example.com")
- mail.execute(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
- end
- end
-
- describe 'successful build and notify only broken builds' do
- let(:project) do
- FactoryGirl.create(:ci_project,
- email_add_pusher: true,
- email_only_broken_builds: true,
- email_recipients: "jeroen@example.com")
- end
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
- let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
-
- before do
- allow(mail).to receive_messages(
- project: project
- )
- end
-
- it do
- should_email(commit.git_author_email)
- should_email("jeroen@example.com")
- mail.execute(build) if mail.can_execute?(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
- end
- end
-
- describe 'successful build and can test service' do
- let(:project) do
- FactoryGirl.create(:ci_project,
- email_add_pusher: true,
- email_only_broken_builds: false,
- email_recipients: "jeroen@example.com")
- end
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
- let(:build) { FactoryGirl.create(:ci_build, status: 'success', commit: commit, user: user) }
-
- before do
- allow(mail).to receive_messages(
- project: project
- )
- build
- end
-
- it do
- expect(mail.can_test?).to eq(true)
- end
- end
-
- describe 'retried build should not receive email' do
- let(:project) do
- FactoryGirl.create(:ci_project,
- email_add_pusher: true,
- email_only_broken_builds: true,
- email_recipients: "jeroen@example.com")
- end
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
- let(:build) { FactoryGirl.create(:ci_build, status: 'failed', commit: commit, user: user) }
-
- before do
- allow(mail).to receive_messages(
- project: project
- )
- end
-
- it do
- Ci::Build.retry(build)
- should_email(commit.git_author_email)
- should_email("jeroen@example.com")
- mail.execute(build) if mail.can_execute?(build)
- end
-
- def should_email(email)
- expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
- expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
- end
- end
- end
-end
diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb
deleted file mode 100644
index 8adda6c86cc..00000000000
--- a/spec/models/ci/project_services/slack_message_spec.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'spec_helper'
-
-describe Ci::SlackMessage do
- subject { Ci::SlackMessage.new(commit) }
-
- let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
-
- context 'when all matrix builds succeeded' do
- let(:color) { 'good' }
-
- it 'returns a message with success' do
- commit.create_builds('master', false, nil)
- commit.builds.update_all(status: "success")
- commit.reload
-
- expect(subject.color).to eq(color)
- expect(subject.fallback).to include('Commit')
- expect(subject.fallback).to include("\##{commit.id}")
- expect(subject.fallback).to include('succeeded')
- expect(subject.attachments.first[:fields]).to be_empty
- end
- end
-
- context 'when one of matrix builds failed' do
- let(:color) { 'danger' }
-
- it 'returns a message with information about failed build' do
- commit.create_builds('master', false, nil)
- first_build = commit.builds.first
- second_build = commit.builds.last
- first_build.update(status: "success")
- second_build.update(status: "failed")
-
- expect(subject.color).to eq(color)
- expect(subject.fallback).to include('Commit')
- expect(subject.fallback).to include("\##{commit.id}")
- expect(subject.fallback).to include('failed')
- expect(subject.attachments.first[:fields].size).to eq(1)
- expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name)
- expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}")
- end
- end
-end
diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb
deleted file mode 100644
index 1ac7dfe568d..00000000000
--- a/spec/models/ci/project_services/slack_service_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-require 'spec_helper'
-
-describe Ci::SlackService do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- end
-
- describe "Validations" do
- context "active" do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of :webhook }
- end
- end
-
- describe "Execute" do
- let(:slack) { Ci::SlackService.new }
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
- let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
- let(:notify_only_broken_builds) { false }
-
- before do
- allow(slack).to receive_messages(
- project: commit.project,
- project_id: commit.project_id,
- webhook: webhook_url,
- notify_only_broken_builds: notify_only_broken_builds
- )
-
- WebMock.stub_request(:post, webhook_url)
- end
-
- it "should call Slack API" do
- slack.execute(build)
- Ci::SlackNotifierWorker.drain
-
- expect(WebMock).to have_requested(:post, webhook_url).once
- end
- end
-end
diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb
deleted file mode 100644
index 490c6a67982..00000000000
--- a/spec/models/ci/project_spec.rb
+++ /dev/null
@@ -1,258 +0,0 @@
-# == Schema Information
-#
-# Table name: projects
-#
-# id :integer not null, primary key
-# name :string(255) not null
-# timeout :integer default(3600), not null
-# created_at :datetime
-# updated_at :datetime
-# token :string(255)
-# default_ref :string(255)
-# path :string(255)
-# always_build :boolean default(FALSE), not null
-# polling_interval :integer
-# public :boolean default(FALSE), not null
-# ssh_url_to_repo :string(255)
-# gitlab_id :integer
-# allow_git_fetch :boolean default(TRUE), not null
-# email_recipients :string(255) default(""), not null
-# email_add_pusher :boolean default(TRUE), not null
-# email_only_broken_builds :boolean default(TRUE), not null
-# skip_refs :string(255)
-# coverage_regex :string(255)
-# shared_runners_enabled :boolean default(FALSE)
-# generated_yaml_config :text
-#
-
-require 'spec_helper'
-
-describe Ci::Project do
- let(:gl_project) { FactoryGirl.create :empty_project }
- let(:project) { FactoryGirl.create :ci_project, gl_project: gl_project }
- subject { project }
-
- it { is_expected.to have_many(:runner_projects) }
- it { is_expected.to have_many(:runners) }
- it { is_expected.to have_many(:web_hooks) }
- it { is_expected.to have_many(:events) }
- it { is_expected.to have_many(:variables) }
- it { is_expected.to have_many(:triggers) }
- it { is_expected.to have_many(:services) }
-
- it { is_expected.to validate_presence_of :timeout }
- it { is_expected.to validate_presence_of :gitlab_id }
-
- describe 'before_validation' do
- it 'should set an random token if none provided' do
- project = FactoryGirl.create :ci_project_without_token
- expect(project.token).not_to eq("")
- end
-
- it 'should not set an random toke if one provided' do
- project = FactoryGirl.create :ci_project
- expect(project.token).to eq("iPWx6WM4lhHNedGfBpPJNP")
- end
- end
-
- describe :name_with_namespace do
- subject { project.name_with_namespace }
-
- it { is_expected.to eq(project.name) }
- it { is_expected.to eq(gl_project.name_with_namespace) }
- end
-
- describe :path_with_namespace do
- subject { project.path_with_namespace }
-
- it { is_expected.to eq(project.path) }
- it { is_expected.to eq(gl_project.path_with_namespace) }
- end
-
- describe :path_with_namespace do
- subject { project.web_url }
-
- it { is_expected.to eq(gl_project.web_url) }
- end
-
- describe :web_url do
- subject { project.web_url }
-
- it { is_expected.to eq(project.gitlab_url) }
- it { is_expected.to eq(gl_project.web_url) }
- end
-
- describe :http_url_to_repo do
- subject { project.http_url_to_repo }
-
- it { is_expected.to eq(gl_project.http_url_to_repo) }
- end
-
- describe :ssh_url_to_repo do
- subject { project.ssh_url_to_repo }
-
- it { is_expected.to eq(gl_project.ssh_url_to_repo) }
- end
-
- describe :commits do
- subject { project.commits }
-
- before do
- FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project
- end
-
- it { is_expected.to eq(gl_project.ci_commits) }
- end
-
- describe :builds do
- subject { project.builds }
-
- before do
- commit = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project
- FactoryGirl.create :ci_build, commit: commit
- end
-
- it { is_expected.to eq(gl_project.ci_builds) }
- end
-
- describe "ordered_by_last_commit_date" do
- it "returns ordered projects" do
- newest_project = FactoryGirl.create :empty_project
- newest_ci_project = newest_project.ensure_gitlab_ci_project
- oldest_project = FactoryGirl.create :empty_project
- oldest_ci_project = oldest_project.ensure_gitlab_ci_project
- project_without_commits = FactoryGirl.create :empty_project
- ci_project_without_commits = project_without_commits.ensure_gitlab_ci_project
-
- FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project
- FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project
-
- expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_ci_project, oldest_ci_project, ci_project_without_commits])
- end
- end
-
- context :valid_project do
- let(:commit) { FactoryGirl.create(:ci_commit) }
-
- context :project_with_commit_and_builds do
- let(:project) { commit.project }
-
- before do
- FactoryGirl.create(:ci_build, commit: commit)
- end
-
- it { expect(project.status).to eq('pending') }
- it { expect(project.last_commit).to be_kind_of(Ci::Commit) }
- it { expect(project.human_status).to eq('pending') }
- end
- end
-
- describe '#email_notification?' do
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: true
- expect(project.email_notification?).to eq(true)
- end
-
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: 'test tesft'
- expect(project.email_notification?).to eq(true)
- end
-
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: ''
- expect(project.email_notification?).to eq(false)
- end
- end
-
- describe '#broken_or_success?' do
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: true
- allow(project).to receive(:broken?).and_return(true)
- allow(project).to receive(:success?).and_return(true)
- expect(project.broken_or_success?).to eq(true)
- end
-
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: true
- allow(project).to receive(:broken?).and_return(true)
- allow(project).to receive(:success?).and_return(false)
- expect(project.broken_or_success?).to eq(true)
- end
-
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: true
- allow(project).to receive(:broken?).and_return(false)
- allow(project).to receive(:success?).and_return(true)
- expect(project.broken_or_success?).to eq(true)
- end
-
- it do
- project = FactoryGirl.create :ci_project, email_add_pusher: true
- allow(project).to receive(:broken?).and_return(false)
- allow(project).to receive(:success?).and_return(false)
- expect(project.broken_or_success?).to eq(false)
- end
- end
-
- describe 'Project.parse' do
- let(:project) { FactoryGirl.create :project }
-
- subject { Ci::Project.parse(project) }
-
- it { is_expected.to be_valid }
- it { is_expected.to be_kind_of(Ci::Project) }
- it { expect(subject.name).to eq(project.name_with_namespace) }
- it { expect(subject.gitlab_id).to eq(project.id) }
- it { expect(subject.gitlab_url).to eq(project.web_url) }
- end
-
- describe :repo_url_with_auth do
- let(:project) { FactoryGirl.create :ci_project }
- subject { project.repo_url_with_auth }
-
- it { is_expected.to be_a(String) }
- it { is_expected.to end_with(".git") }
- it { is_expected.to start_with(project.gitlab_url[0..6]) }
- it { is_expected.to include(project.token) }
- it { is_expected.to include('gitlab-ci-token') }
- it { is_expected.to include(project.gitlab_url[7..-1]) }
- end
-
- describe :any_runners do
- it "there are no runners available" do
- project = FactoryGirl.create(:ci_project)
- expect(project.any_runners?).to be_falsey
- end
-
- it "there is a specific runner" do
- project = FactoryGirl.create(:ci_project)
- project.runners << FactoryGirl.create(:ci_specific_runner)
- expect(project.any_runners?).to be_truthy
- end
-
- it "there is a shared runner" do
- project = FactoryGirl.create(:ci_project, shared_runners_enabled: true)
- FactoryGirl.create(:ci_shared_runner)
- expect(project.any_runners?).to be_truthy
- end
-
- it "there is a shared runner, but they are prohibited to use" do
- project = FactoryGirl.create(:ci_project)
- FactoryGirl.create(:ci_shared_runner)
- expect(project.any_runners?).to be_falsey
- end
-
- it "checks the presence of specific runner" do
- project = FactoryGirl.create(:ci_project)
- specific_runner = FactoryGirl.create(:ci_specific_runner)
- project.runners << specific_runner
- expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
- end
-
- it "checks the presence of shared runner" do
- project = FactoryGirl.create(:ci_project, shared_runners_enabled: true)
- shared_runner = FactoryGirl.create(:ci_shared_runner)
- expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
- end
- end
-end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
index 0218d484130..da8491357a5 100644
--- a/spec/models/ci/runner_project_spec.rb
+++ b/spec/models/ci/runner_project_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runner_projects
+# Table name: ci_runner_projects
#
# id :integer not null, primary key
# runner_id :integer not null
@@ -11,6 +11,6 @@
require 'spec_helper'
-describe Ci::RunnerProject do
+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 f8a51c29dc2..232760dfeba 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: runners
+# Table name: ci_runners
#
# id :integer not null, primary key
# token :string(255)
@@ -19,7 +19,7 @@
require 'spec_helper'
-describe Ci::Runner do
+describe Ci::Runner, models: true do
describe '#display_name' do
it 'should return the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
@@ -38,7 +38,7 @@ describe Ci::Runner do
end
describe :assign_to do
- let!(:project) { FactoryGirl.create :ci_project }
+ let!(:project) { FactoryGirl.create :empty_project }
let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) }
before { shared_runner.assign_to(project) }
@@ -116,8 +116,8 @@ describe Ci::Runner do
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
runner = FactoryGirl.create(:ci_specific_runner)
- project = FactoryGirl.create(:ci_project)
- project1 = FactoryGirl.create(:ci_project)
+ project = FactoryGirl.create(:empty_project)
+ project1 = FactoryGirl.create(:empty_project)
project.runners << runner
project1.runners << runner
@@ -126,7 +126,7 @@ describe Ci::Runner do
it "returns true" do
runner = FactoryGirl.create(:ci_specific_runner)
- project = FactoryGirl.create(:ci_project)
+ project = FactoryGirl.create(:empty_project)
project.runners << runner
expect(runner.belongs_to_one_project?).to be_truthy
diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb
deleted file mode 100644
index 2df70e88888..00000000000
--- a/spec/models/ci/service_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-# == Schema Information
-#
-# Table name: services
-#
-# id :integer not null, primary key
-# type :string(255)
-# title :string(255)
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-# active :boolean default(FALSE), not null
-# properties :text
-#
-
-require 'spec_helper'
-
-describe Ci::Service do
-
- describe "Associations" do
- it { is_expected.to belong_to :project }
- end
-
- describe "Mass assignment" do
- end
-
- describe "Test Button" do
- before do
- @service = Ci::Service.new
- end
-
- describe "Testable" do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
-
- before do
- allow(@service).to receive_messages(
- project: commit.project
- )
- build
- @testable = @service.can_test?
- end
-
- describe :can_test do
- it { expect(@testable).to eq(true) }
- end
- end
- end
-end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 19c14ef2da2..cb2f51e2011 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -1,7 +1,19 @@
+# == Schema Information
+#
+# Table name: ci_triggers
+#
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer not null
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+#
+
require 'spec_helper'
-describe Ci::Trigger do
- let(:project) { FactoryGirl.create :ci_project }
+describe Ci::Trigger, models: true do
+ let(:project) { FactoryGirl.create :empty_project }
describe 'before_validation' do
it 'should set an random token if none provided' do
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index d034a6c7b9f..31b56953a13 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -1,6 +1,6 @@
# == Schema Information
#
-# Table name: variables
+# Table name: ci_variables
#
# id :integer not null, primary key
# project_id :integer not null
@@ -13,7 +13,7 @@
require 'spec_helper'
-describe Ci::Variable do
+describe Ci::Variable, models: true do
subject { Ci::Variable.new }
let(:secret_value) { 'secret' }
diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb
deleted file mode 100644
index bf9481ab81d..00000000000
--- a/spec/models/ci/web_hook_spec.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-# == Schema Information
-#
-# Table name: web_hooks
-#
-# id :integer not null, primary key
-# url :string(255) not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
-#
-
-require 'spec_helper'
-
-describe Ci::WebHook do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- end
-
- describe "Validations" do
- it { is_expected.to validate_presence_of(:url) }
-
- context "url format" do
- it { is_expected.to allow_value("http://example.com").for(:url) }
- it { is_expected.to allow_value("https://excample.com").for(:url) }
- it { is_expected.to allow_value("http://test.com/api").for(:url) }
- it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
- it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
-
- it { is_expected.not_to allow_value("example.com").for(:url) }
- it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
- it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
- end
- end
-
- describe "execute" do
- before(:each) do
- @web_hook = FactoryGirl.create(:ci_web_hook)
- @project = @web_hook.project
- @data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
-
- WebMock.stub_request(:post, @web_hook.url)
- end
-
- it "POSTs to the web hook URL" do
- @web_hook.execute(@data)
- expect(WebMock).to have_requested(:post, @web_hook.url).once
- end
-
- it "POSTs the data as JSON" do
- json = @data.to_json
-
- @web_hook.execute(@data)
- expect(WebMock).to have_requested(:post, @web_hook.url).with(body: json).once
- end
-
- it "catches exceptions" do
- expect(Ci::WebHook).to receive(:post).and_raise("Some HTTP Post error")
-
- expect{ @web_hook.execute(@data) }.
- to raise_error(RuntimeError, 'Some HTTP Post error')
- end
- end
-end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
index 1031af097bd..9307d97e214 100644
--- a/spec/models/commit_range_spec.rb
+++ b/spec/models/commit_range_spec.rb
@@ -1,56 +1,78 @@
require 'spec_helper'
-describe CommitRange do
+describe CommitRange, models: true do
describe 'modules' do
subject { described_class }
it { is_expected.to include_module(Referable) }
end
- let(:sha_from) { 'f3f85602' }
- let(:sha_to) { 'e86e1013' }
+ let!(:project) { create(:project, :public) }
+ let!(:commit1) { project.commit("HEAD~2") }
+ let!(:commit2) { project.commit }
- let(:range) { described_class.new("#{sha_from}...#{sha_to}") }
- let(:range2) { described_class.new("#{sha_from}..#{sha_to}") }
+ let(:sha_from) { commit1.short_id }
+ let(:sha_to) { commit2.short_id }
+
+ let(:full_sha_from) { commit1.id }
+ let(:full_sha_to) { commit2.id }
+
+ let(:range) { described_class.new("#{sha_from}...#{sha_to}", project) }
+ let(:range2) { described_class.new("#{sha_from}..#{sha_to}", project) }
it 'raises ArgumentError when given an invalid range string' do
- expect { described_class.new("Foo") }.to raise_error(ArgumentError)
+ expect { described_class.new("Foo", project) }.to raise_error(ArgumentError)
end
describe '#to_s' do
it 'is correct for three-dot syntax' do
- expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}"
+ expect(range.to_s).to eq "#{full_sha_from}...#{full_sha_to}"
end
it 'is correct for two-dot syntax' do
- expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}"
+ expect(range2.to_s).to eq "#{full_sha_from}..#{full_sha_to}"
end
end
describe '#to_reference' do
- let(:project) { double('project', to_reference: 'namespace1/project') }
+ let(:cross) { create(:project) }
+
+ it 'returns a String reference to the object' do
+ expect(range.to_reference).to eq "#{full_sha_from}...#{full_sha_to}"
+ end
+
+ it 'returns a String reference to the object' do
+ expect(range2.to_reference).to eq "#{full_sha_from}..#{full_sha_to}"
+ end
+
+ it 'supports a cross-project reference' do
+ expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{full_sha_from}...#{full_sha_to}"
+ end
+ end
- before do
- range.project = project
+ describe '#reference_link_text' do
+ let(:cross) { create(:project) }
+
+ it 'returns a String reference to the object' do
+ expect(range.reference_link_text).to eq "#{sha_from}...#{sha_to}"
end
it 'returns a String reference to the object' do
- expect(range.to_reference).to eq range.to_s
+ expect(range2.reference_link_text).to eq "#{sha_from}..#{sha_to}"
end
it 'supports a cross-project reference' do
- cross = double('project')
- expect(range.to_reference(cross)).to eq "#{project.to_reference}@#{range.to_s}"
+ expect(range.reference_link_text(cross)).to eq "#{project.to_reference}@#{sha_from}...#{sha_to}"
end
end
describe '#reference_title' do
it 'returns the correct String for three-dot ranges' do
- expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}"
+ expect(range.reference_title).to eq "Commits #{full_sha_from} through #{full_sha_to}"
end
it 'returns the correct String for two-dot ranges' do
- expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}"
+ expect(range2.reference_title).to eq "Commits #{full_sha_from}^ through #{full_sha_to}"
end
end
@@ -60,11 +82,11 @@ describe CommitRange do
end
it 'includes the correct values for a three-dot range' do
- expect(range.to_param).to eq({ from: sha_from, to: sha_to })
+ expect(range.to_param).to eq({ from: full_sha_from, to: full_sha_to })
end
it 'includes the correct values for a two-dot range' do
- expect(range2.to_param).to eq({ from: sha_from + '^', to: sha_to })
+ expect(range2.to_param).to eq({ from: full_sha_from + '^', to: full_sha_to })
end
end
@@ -79,64 +101,37 @@ describe CommitRange do
end
describe '#valid_commits?' do
- context 'without a project' do
- it 'returns nil' do
- expect(range.valid_commits?).to be_nil
+ context 'with a valid repo' do
+ before do
+ expect(project).to receive(:valid_repo?).and_return(true)
end
- end
-
- it 'accepts an optional project argument' do
- project1 = double('project1').as_null_object
- project2 = double('project2').as_null_object
-
- # project1 gets assigned through the accessor, but ignored when not given
- # as an argument to `valid_commits?`
- expect(project1).not_to receive(:present?)
- range.project = project1
-
- # project2 gets passed to `valid_commits?`
- expect(project2).to receive(:present?).and_return(false)
- range.valid_commits?(project2)
- end
-
- context 'with a project' do
- let(:project) { double('project', repository: double('repository')) }
+ it 'is false when `sha_from` is invalid' do
+ expect(project).to receive(:commit).with(sha_from).and_return(nil)
+ expect(project).to receive(:commit).with(sha_to).and_call_original
- context 'with a valid repo' do
- before do
- expect(project).to receive(:valid_repo?).and_return(true)
- range.project = project
- end
+ expect(range).not_to be_valid_commits
+ end
- it 'is false when `sha_from` is invalid' do
- expect(project.repository).to receive(:commit).with(sha_from).and_return(false)
- expect(project.repository).not_to receive(:commit).with(sha_to)
- expect(range).not_to be_valid_commits
- end
+ it 'is false when `sha_to` is invalid' do
+ expect(project).to receive(:commit).with(sha_from).and_call_original
+ expect(project).to receive(:commit).with(sha_to).and_return(nil)
- it 'is false when `sha_to` is invalid' do
- expect(project.repository).to receive(:commit).with(sha_from).and_return(true)
- expect(project.repository).to receive(:commit).with(sha_to).and_return(false)
- expect(range).not_to be_valid_commits
- end
+ expect(range).not_to be_valid_commits
+ end
- it 'is true when both `sha_from` and `sha_to` are valid' do
- expect(project.repository).to receive(:commit).with(sha_from).and_return(true)
- expect(project.repository).to receive(:commit).with(sha_to).and_return(true)
- expect(range).to be_valid_commits
- end
+ it 'is true when both `sha_from` and `sha_to` are valid' do
+ expect(range).to be_valid_commits
end
+ end
- context 'without a valid repo' do
- before do
- expect(project).to receive(:valid_repo?).and_return(false)
- range.project = project
- end
+ context 'without a valid repo' do
+ before do
+ expect(project).to receive(:valid_repo?).and_return(false)
+ end
- it 'returns false' do
- expect(range).not_to be_valid_commits
- end
+ it 'returns false' do
+ expect(range).not_to be_valid_commits
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 90be9324951..ecf37b40c58 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Commit do
+describe Commit, models: true do
let(:project) { create(:project) }
let(:commit) { project.commit }
@@ -24,6 +24,17 @@ describe Commit do
end
end
+ describe '#reference_link_text' do
+ it 'returns a String reference to the object' do
+ expect(commit.reference_link_text).to eq commit.short_id
+ end
+
+ it 'supports a cross-project reference' do
+ cross = double('project')
+ expect(commit.reference_link_text(cross)).to eq "#{project.to_reference}@#{commit.short_id}"
+ end
+ end
+
describe '#title' do
it "returns no_commit_message when safe_message is blank" do
allow(commit).to receive(:safe_message).and_return('')
@@ -77,14 +88,10 @@ eos
let(:other_issue) { create :issue, project: other_project }
it 'detects issues that this commit is marked as closing' do
- allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid}")
- expect(commit.closes_issues).to eq([issue])
- end
-
- it 'does not detect issues from other projects' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
- allow(commit).to receive(:safe_message).and_return("Fixes #{ext_ref}")
- expect(commit.closes_issues).to be_empty
+ allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}")
+ expect(commit.closes_issues).to include(issue)
+ expect(commit.closes_issues).to include(other_issue)
end
end
@@ -100,4 +107,15 @@ eos
# Include the subject in the repository stub.
let(:extra_commits) { [subject] }
end
+
+ describe '#hook_attrs' do
+ let(:data) { commit.hook_attrs(with_changed_files: true) }
+
+ it { expect(data).to be_a(Hash) }
+ it { expect(data[:message]).to include('Add submodule from gitlab.com') }
+ it { expect(data[:timestamp]).to eq('2014-02-27T11:01:38+02:00') }
+ it { expect(data[:added]).to eq(["gitlab-grack"]) }
+ it { expect(data[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:removed]).to eq([]) }
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index c96a606fdaa..b8f901b3433 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,17 +1,51 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
require 'spec_helper'
-describe CommitStatus do
+describe CommitStatus, models: true do
let(:commit) { FactoryGirl.create :ci_commit }
let(:commit_status) { FactoryGirl.create :commit_status, commit: commit }
it { is_expected.to belong_to(:commit) }
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(:gl_project).to(:commit) }
it { is_expected.to respond_to :success? }
it { is_expected.to respond_to :failed? }
diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb
index f7ed30f8198..25b3f4e50da 100644
--- a/spec/models/concerns/case_sensitivity_spec.rb
+++ b/spec/models/concerns/case_sensitivity_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe CaseSensitivity do
+describe CaseSensitivity, models: true do
describe '.iwhere' do
let(:connection) { ActiveRecord::Base.connection }
let(:model) { Class.new { include CaseSensitivity } }
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 0f13c4410cd..021d62cdf0c 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -81,4 +81,36 @@ describe Issue, "Issuable" do
expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
end
end
+
+ describe '#card_attributes' do
+ it 'includes the author name' do
+ allow(issue).to receive(:author).and_return(double(name: 'Robert'))
+ allow(issue).to receive(:assignee).and_return(nil)
+
+ expect(issue.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => nil })
+ end
+
+ it 'includes the assignee name' do
+ allow(issue).to receive(:author).and_return(double(name: 'Robert'))
+ allow(issue).to receive(:assignee).and_return(double(name: 'Douwe'))
+
+ expect(issue.card_attributes).
+ to eq({ 'Author' => 'Robert', 'Assignee' => 'Douwe' })
+ end
+ end
+
+ describe "votes" do
+ 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)
+ end
+
+ it "returns correct values" do
+ expect(issue.upvotes).to eq(1)
+ expect(issue.downvotes).to eq(1)
+ end
+ end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 6179882e935..20f0c561e44 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -1,5 +1,22 @@
require 'spec_helper'
+describe Mentionable do
+ include Mentionable
+
+ def author
+ nil
+ end
+
+ describe :references do
+ let(:project) { create(:project) }
+
+ it 'excludes JIRA references' do
+ allow(project).to receive_messages(jira_tracker?: true)
+ expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
+ end
+ end
+end
+
describe Issue, "Mentionable" do
describe '#mentioned_users' do
let!(:user) { create(:user, username: 'stranger') }
diff --git a/spec/models/concerns/strip_attribute_spec.rb b/spec/models/concerns/strip_attribute_spec.rb
new file mode 100644
index 00000000000..6445e29c3ef
--- /dev/null
+++ b/spec/models/concerns/strip_attribute_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe Milestone, "StripAttribute" do
+ let(:milestone) { create(:milestone) }
+
+ describe ".strip_attributes" do
+ it { expect(Milestone).to respond_to(:strip_attributes) }
+ it { expect(Milestone.strip_attrs).to include(:title) }
+ end
+
+ describe "#strip_attributes" do
+ before do
+ milestone.title = ' 8.3 '
+ milestone.valid?
+ end
+
+ it { expect(milestone.title).to eq('8.3') }
+ end
+
+end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
new file mode 100644
index 00000000000..30c0a04b840
--- /dev/null
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+shared_examples 'TokenAuthenticatable' do
+ describe 'dynamically defined methods' do
+ it { expect(described_class).to be_private_method_defined(:generate_token) }
+ it { expect(described_class).to be_private_method_defined(:write_new_token) }
+ it { expect(described_class).to respond_to("find_by_#{token_field}") }
+ it { is_expected.to respond_to("ensure_#{token_field}") }
+ it { is_expected.to respond_to("reset_#{token_field}!") }
+ end
+end
+
+describe User, 'TokenAuthenticatable' do
+ let(:token_field) { :authentication_token }
+ it_behaves_like 'TokenAuthenticatable'
+
+ describe 'ensures authentication token' do
+ subject { create(:user).send(token_field) }
+ it { is_expected.to be_a String }
+ end
+end
+
+describe ApplicationSetting, 'TokenAuthenticatable' do
+ let(:token_field) { :runners_registration_token }
+ it_behaves_like 'TokenAuthenticatable'
+
+ describe 'generating new token' 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 }
+ 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 }
+ end
+
+ describe 'ensured! token' do
+ subject { described_class.new.send("ensure_#{token_field}!") }
+
+ it 'should persist new token' do
+ expect(subject).to eq described_class.current[token_field]
+ end
+ end
+ end
+
+ context 'token is generated' do
+ before { subject.send("reset_#{token_field}!") }
+ it 'persists a new token 'do
+ expect(subject.send(:read_attribute, token_field)).to be_a String
+ end
+ end
+ end
+
+ describe 'multiple token fields' do
+ before do
+ described_class.send(:add_authentication_token_field, :yet_another_token)
+ end
+
+ describe '.token_fields' do
+ subject { described_class.authentication_token_fields }
+ it { is_expected.to include(:runners_registration_token, :yet_another_token) }
+ end
+ end
+end
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 95729932459..64ba778afea 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -15,7 +15,7 @@
require 'spec_helper'
-describe DeployKey do
+describe DeployKey, models: true do
let(:project) { create(:project) }
let(:deploy_key) { create(:deploy_key, projects: [project]) }
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index 0eb22599d18..8aedbfb8636 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -11,7 +11,7 @@
require 'spec_helper'
-describe DeployKeysProject do
+describe DeployKeysProject, models: true do
describe "Associations" do
it { is_expected.to belong_to(:deploy_key) }
it { is_expected.to belong_to(:project) }
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 0f32f162a10..071582b0282 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -16,7 +16,7 @@
require 'spec_helper'
-describe Event do
+describe Event, models: true do
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:target) }
@@ -64,4 +64,42 @@ describe Event do
it { expect(@event.branch_name).to eq("master") }
it { expect(@event.author).to eq(@user) }
end
+
+ describe '.latest_update_time' do
+ describe 'when events are present' do
+ let(:time) { Time.utc(2015, 1, 1) }
+
+ before do
+ create(:closed_issue_event, updated_at: time)
+ create(:closed_issue_event, updated_at: time + 5)
+ end
+
+ it 'returns the latest update time' do
+ expect(Event.latest_update_time).to eq(time + 5)
+ end
+ end
+
+ describe 'when no events exist' do
+ it 'returns nil' do
+ expect(Event.latest_update_time).to be_nil
+ end
+ end
+ end
+
+ describe '.limit_recent' do
+ let!(:event1) { create(:closed_issue_event) }
+ let!(:event2) { create(:closed_issue_event) }
+
+ describe 'without an explicit limit' do
+ subject { Event.limit_recent }
+
+ it { is_expected.to eq([event2, event1]) }
+ end
+
+ describe 'with an explicit limit' do
+ subject { Event.limit_recent(1) }
+
+ it { is_expected.to eq([event2]) }
+ end
+ end
end
diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb
index 7744610db78..6ec6b9037a4 100644
--- a/spec/models/external_issue_spec.rb
+++ b/spec/models/external_issue_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe ExternalIssue do
+describe ExternalIssue, models: true do
let(:project) { double('project', to_reference: 'namespace1/project1') }
let(:issue) { described_class.new('EXT-1234', project) }
diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb
index 4bd5b0be61c..b198aa77526 100644
--- a/spec/models/external_wiki_service_spec.rb
+++ b/spec/models/external_wiki_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe ExternalWikiService do
+describe ExternalWikiService, models: true do
include ExternalWikiHelper
describe "Associations" do
it { should belong_to :project }
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index f442fa5fbe5..d61c1c96bde 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -1,6 +1,39 @@
+# == Schema Information
+#
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+#
+
require 'spec_helper'
-describe GenericCommitStatus do
+describe GenericCommitStatus, models: true do
let(:commit) { FactoryGirl.create :ci_commit }
let(:generic_commit_status) { FactoryGirl.create :generic_commit_status, commit: commit }
diff --git a/spec/models/global_milestone_spec.rb b/spec/models/global_milestone_spec.rb
new file mode 100644
index 00000000000..197c99cd007
--- /dev/null
+++ b/spec/models/global_milestone_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe GlobalMilestone, models: true do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project1) { create(:project, group: group) }
+ let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
+ let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
+ let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
+ let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
+ let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
+ let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
+ let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
+ let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
+
+ describe :build_collection do
+ before do
+ milestones =
+ [
+ milestone1_project1,
+ milestone1_project2,
+ milestone1_project3,
+ milestone2_project1,
+ milestone2_project2,
+ milestone2_project3
+ ]
+
+ @global_milestones = GlobalMilestone.build_collection(milestones)
+ end
+
+ it 'should have all project milestones' do
+ expect(@global_milestones.count).to eq(2)
+ end
+
+ it 'should have all project milestones titles' do
+ expect(@global_milestones.map(&:title)).to match_array(['Milestone v1.2', 'VD-123'])
+ end
+
+ it 'should have all project milestones' do
+ expect(@global_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
+ end
+ end
+
+ describe :initialize do
+ before do
+ milestones =
+ [
+ milestone1_project1,
+ milestone1_project2,
+ milestone1_project3,
+ ]
+
+ @global_milestone = GlobalMilestone.new(milestone1_project1.title, milestones)
+ end
+
+ it 'should have exactly one group milestone' do
+ expect(@global_milestone.title).to eq('Milestone v1.2')
+ end
+
+ it 'should have all project milestones with the same title' do
+ expect(@global_milestone.milestones.count).to eq(3)
+ end
+ end
+
+ describe :safe_title do
+ let(:milestone) { create(:milestone, title: "git / test", project: project1) }
+
+ it 'should strip out slashes and spaces' do
+ global_milestone = GlobalMilestone.new(milestone.title, [milestone])
+
+ expect(global_milestone.safe_title).to eq('git-test')
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 80638fc8db2..ba5acceadff 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -11,11 +11,12 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
require 'spec_helper'
-describe Group do
+describe Group, models: true do
let!(:group) { create(:group) }
describe 'associations' do
@@ -37,6 +38,25 @@ describe Group do
it { is_expected.not_to validate_presence_of :owner }
end
+ describe '.visible_to_user' do
+ let!(:group) { create(:group) }
+ let!(:user) { create(:user) }
+
+ subject { described_class.visible_to_user(user) }
+
+ describe 'when the user has access to a group' do
+ before do
+ group.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it { is_expected.to eq([group]) }
+ end
+
+ describe 'when the user does not have access to any groups' do
+ it { is_expected.to eq([]) }
+ end
+ end
+
describe '#to_reference' do
it 'returns a String reference to the object' do
expect(group.to_reference).to eq "@#{group.name}"
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index a2dc66fce3e..645ee0b929a 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -18,7 +18,7 @@
require 'spec_helper'
-describe ProjectHook do
+describe ProjectHook, models: true do
describe '.push_hooks' do
it 'should return hooks for push events only' do
hook = create(:project_hook, push_events: true)
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 16641c12124..1455661485b 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -18,7 +18,7 @@
require "spec_helper"
-describe ServiceHook do
+describe ServiceHook, models: true do
describe "Associations" do
it { is_expected.to belong_to :service }
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 02d2cc2c77a..138b87a9a06 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -18,7 +18,7 @@
require "spec_helper"
-describe SystemHook do
+describe SystemHook, models: true do
describe "execute" do
before(:each) do
@system_hook = create(:system_hook)
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 2fdc49f02ee..2d90b0793cc 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -18,7 +18,7 @@
require 'spec_helper'
-describe ProjectHook do
+describe ProjectHook, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
end
@@ -71,5 +71,11 @@ describe ProjectHook do
expect { @project_hook.execute(@data, 'push_hooks') }.to raise_error(RuntimeError)
end
+
+ it "handles SSL exceptions" do
+ expect(WebHook).to receive(:post).and_raise(OpenSSL::SSL::SSLError.new('SSL error'))
+
+ expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
+ end
end
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index c9aa1b063c6..52271c7c8c6 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe Issue do
+describe Issue, models: true do
describe "Associations" do
it { is_expected.to belong_to(:milestone) }
end
diff --git a/spec/models/jira_issue_spec.rb b/spec/models/jira_issue_spec.rb
new file mode 100644
index 00000000000..1634265b439
--- /dev/null
+++ b/spec/models/jira_issue_spec.rb
@@ -0,0 +1,30 @@
+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 2f819f60cbb..c962b83644a 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -15,7 +15,7 @@
require 'spec_helper'
-describe Key do
+describe Key, models: true do
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
@@ -81,7 +81,7 @@ describe Key do
it 'rejects the multiple line key' do
key = build(:key)
- key.key.gsub!(' ', "\n")
+ key.key.tr!(' ', "\n")
expect(key).not_to be_valid
end
end
diff --git a/spec/models/label_link_spec.rb b/spec/models/label_link_spec.rb
index 8c240826582..dc7510b1de3 100644
--- a/spec/models/label_link_spec.rb
+++ b/spec/models/label_link_spec.rb
@@ -12,7 +12,7 @@
require 'spec_helper'
-describe LabelLink do
+describe LabelLink, models: true do
let(:label) { create(:label_link) }
it { expect(label).to be_valid }
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index 6518213d71c..696fbf7e0aa 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -8,11 +8,12 @@
# project_id :integer
# created_at :datetime
# updated_at :datetime
+# template :boolean default(FALSE)
#
require 'spec_helper'
-describe Label do
+describe Label, models: true do
let(:label) { create(:label) }
describe 'associations' do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 57f840c1e91..2aedca20df2 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -19,7 +19,7 @@
require 'spec_helper'
-describe Member do
+describe Member, models: true do
describe "Associations" do
it { is_expected.to belong_to(:user) }
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 652026729bb..5424c9b9cba 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -19,7 +19,7 @@
require 'spec_helper'
-describe GroupMember do
+describe GroupMember, models: true do
context 'notification' do
describe "#after_create" do
it "should send email to user" do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index ee912bf12a2..9f26d9eb5ce 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -19,7 +19,7 @@
require 'spec_helper'
-describe ProjectMember do
+describe ProjectMember, models: true do
describe :import_team do
before do
@abilities = Six.new
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6aaf1c036b0..e0653a8327d 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -20,17 +20,18 @@
# position :integer default(0)
# locked_at :datetime
# updated_by_id :integer
+# merge_error :string(255)
#
require 'spec_helper'
-describe MergeRequest do
+describe MergeRequest, models: true do
subject { create(:merge_request) }
describe 'associations' do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
-
+ it { is_expected.to belong_to(:merge_user).class_name("User") }
it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
end
@@ -47,12 +48,32 @@ describe MergeRequest do
describe 'validation' do
it { is_expected.to validate_presence_of(:target_branch) }
it { is_expected.to validate_presence_of(:source_branch) }
+
+ context "Validation of merge user with Merge When Build succeeds" do
+ it "allows user to be nil when the feature is disabled" do
+ expect(subject).to be_valid
+ end
+
+ it "is invalid without merge user" do
+ subject.merge_when_build_succeeds = true
+ expect(subject).not_to be_valid
+ end
+
+ it "is valid with merge user" do
+ subject.merge_when_build_succeeds = true
+ subject.merge_user = build(:user)
+
+ expect(subject).to be_valid
+ end
+ end
end
describe 'respond to' do
it { is_expected.to respond_to(:unchecked?) }
it { is_expected.to respond_to(:can_be_merged?) }
it { is_expected.to respond_to(:cannot_be_merged?) }
+ it { is_expected.to respond_to(:merge_params) }
+ it { is_expected.to respond_to(:merge_when_build_succeeds) }
end
describe '#to_reference' do
@@ -79,6 +100,12 @@ describe MergeRequest do
expect(merge_request.commits).not_to be_empty
expect(merge_request.mr_and_commit_notes.count).to eq(2)
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)
+ expect(merge_request.commits).not_to be_empty
+ expect(merge_request.mr_and_commit_notes.count).to eq(3)
+ end
end
describe '#is_being_reassigned?' do
@@ -137,6 +164,17 @@ describe MergeRequest do
expect(subject.closes_issues).to include(issue2)
end
+
+ context 'for a project with JIRA integration' do
+ let(:issue0) { JiraIssue.new('JIRA-123', subject.project) }
+ let(:issue1) { JiraIssue.new('FOOBAR-4567', subject.project) }
+
+ it 'returns sorted JiraIssues' do
+ allow(subject.project).to receive_messages(default_branch: subject.target_branch)
+
+ expect(subject.closes_issues).to eq([issue0, issue1])
+ end
+ end
end
describe "#work_in_progress?" do
@@ -165,6 +203,50 @@ describe MergeRequest do
end
end
+ describe '#can_remove_source_branch?' do
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+
+ before do
+ subject.source_project.team << [user, :master]
+
+ subject.source_branch = "feature"
+ subject.target_branch = "master"
+ subject.save!
+ end
+
+ it "can't be removed when its a protected branch" do
+ allow(subject.source_project).to receive(:protected_branch?).and_return(true)
+ expect(subject.can_remove_source_branch?(user)).to be_falsey
+ end
+
+ it "cant remove a root ref" do
+ subject.source_branch = "master"
+ subject.target_branch = "feature"
+
+ expect(subject.can_remove_source_branch?(user)).to be_falsey
+ end
+
+ it "is unable to remove the source branch for a project the user cannot push to" do
+ expect(subject.can_remove_source_branch?(user2)).to be_falsey
+ end
+
+ it "is can be removed in all other cases" do
+ expect(subject.can_remove_source_branch?(user)).to be_truthy
+ 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) }
+
+ 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
+ end
+ end
+
describe "#hook_attrs" do
it "has all the required keys" do
attrs = subject.hook_attrs
@@ -186,4 +268,29 @@ describe MergeRequest do
it_behaves_like 'a Taskable' do
subject { create :merge_request, :simple }
end
+
+ describe '#ci_commit' do
+ describe 'when the source project exists' do
+ it 'returns the latest commit' do
+ commit = double(:commit, id: '123abc')
+ ci_commit = double(:ci_commit)
+
+ allow(subject).to receive(:last_commit).and_return(commit)
+
+ expect(subject.source_project).to receive(:ci_commit).
+ with('123abc').
+ and_return(ci_commit)
+
+ expect(subject.ci_commit).to eq(ci_commit)
+ end
+ end
+
+ describe 'when the source project does not exist' do
+ it 'returns nil' do
+ allow(subject).to receive(:source_project).and_return(nil)
+
+ expect(subject.ci_commit).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 77c58627322..30a71987d86 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -15,7 +15,7 @@
require 'spec_helper'
-describe Milestone do
+describe Milestone, models: true do
describe "Associations" do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_many(:issues) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 1d72a9503ae..4fa2d2bc4d2 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -11,11 +11,12 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
+# public :boolean default(FALSE)
#
require 'spec_helper'
-describe Namespace do
+describe Namespace, models: true do
let!(:namespace) { create(:namespace) }
it { is_expected.to have_many :projects }
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 75564839dcf..593d8f76215 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -16,11 +16,12 @@
# system :boolean default(FALSE), not null
# st_diff :text
# updated_by_id :integer
+# is_award :boolean default(FALSE), not null
#
require 'spec_helper'
-describe Note do
+describe Note, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to belong_to(:noteable) }
@@ -32,77 +33,6 @@ describe Note do
it { is_expected.to validate_presence_of(:project) }
end
- describe '#votable?' do
- it 'is true for issue notes' do
- note = build(:note_on_issue)
- expect(note).to be_votable
- end
-
- it 'is true for merge request notes' do
- note = build(:note_on_merge_request)
- expect(note).to be_votable
- end
-
- it 'is false for merge request diff notes' do
- note = build(:note_on_merge_request_diff)
- expect(note).not_to be_votable
- end
-
- it 'is false for commit notes' do
- note = build(:note_on_commit)
- expect(note).not_to be_votable
- end
-
- it 'is false for commit diff notes' do
- note = build(:note_on_commit_diff)
- expect(note).not_to be_votable
- end
- end
-
- describe 'voting score' do
- it 'recognizes a neutral note' do
- note = build(:votable_note, note: 'This is not a +1 note')
- expect(note).not_to be_upvote
- expect(note).not_to be_downvote
- end
-
- it 'recognizes a neutral emoji note' do
- note = build(:votable_note, note: "I would :+1: this, but I don't want to")
- expect(note).not_to be_upvote
- expect(note).not_to be_downvote
- end
-
- it 'recognizes a +1 note' do
- note = build(:votable_note, note: '+1 for this')
- expect(note).to be_upvote
- end
-
- it 'recognizes a +1 emoji as a vote' do
- note = build(:votable_note, note: ':+1: for this')
- expect(note).to be_upvote
- end
-
- it 'recognizes a thumbsup emoji as a vote' do
- note = build(:votable_note, note: ':thumbsup: for this')
- expect(note).to be_upvote
- end
-
- it 'recognizes a -1 note' do
- note = build(:votable_note, note: '-1 for this')
- expect(note).to be_downvote
- end
-
- it 'recognizes a -1 emoji as a vote' do
- note = build(:votable_note, note: ':-1: for this')
- expect(note).to be_downvote
- end
-
- it 'recognizes a thumbsdown emoji as a vote' do
- note = build(:votable_note, note: ':thumbsdown: for this')
- expect(note).to be_downvote
- end
- end
-
describe "Commit notes" do
let!(:note) { create(:note_on_commit, note: "+1 from me") }
let!(:commit) { note.noteable }
@@ -139,10 +69,6 @@ describe Note do
it "should be recognized by #for_commit_diff_line?" do
expect(note).to be_for_commit_diff_line
end
-
- it "should not be votable" do
- expect(note).not_to be_votable
- end
end
describe 'authorization' do
@@ -204,4 +130,47 @@ describe Note do
it { expect(Note.search('wow')).to include(note) }
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 "editable?" do
+ it "returns true" do
+ note = build(:note)
+ expect(note.editable?).to be_truthy
+ end
+
+ it "returns false" 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 "set_award!" do
+ let(:issue) { create :issue }
+
+ it "converts aliases to actual name" do
+ note = create :note, note: ":+1:", noteable: issue
+ expect(note.reload.note).to eq("thumbsup")
+ end
+ end
end
diff --git a/spec/models/project_security_spec.rb b/spec/models/project_security_spec.rb
index f600a240c46..3643ad1b052 100644
--- a/spec/models/project_security_spec.rb
+++ b/spec/models/project_security_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Project do
+describe Project, models: true do
describe :authorization do
before do
@p1 = create(:project)
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 64bb92fba95..f3d15f3c1ea 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -40,6 +40,20 @@ describe AsanaService, models: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
+ def create_data_for_commits(*messages)
+ {
+ object_kind: 'push',
+ ref: 'master',
+ user_name: user.name,
+ commits: messages.map do |m|
+ {
+ message: m,
+ url: 'https://gitlab.com/',
+ }
+ end
+ }
+ end
+
before do
@asana = AsanaService.new
allow(@asana).to receive_messages(
@@ -51,16 +65,67 @@ describe AsanaService, models: true do
)
end
- it 'should call Asana service to created a story' do
- expect(Asana::Task).to receive(:find).with('123456').once
+ it 'should call Asana service to create a story' do
+ data = create_data_for_commits('Message from commit. related to #123456')
+ expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
- @asana.check_commit('related to #123456', 'pushed')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment).with(text: expected_message)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1)
+
+ @asana.execute(data)
end
- it 'should call Asana service to created a story and close a task' do
- expect(Asana::Task).to receive(:find).with('456789').twice
+ it 'should call Asana service to create a story and close a task' do
+ data = create_data_for_commits('fix #456789')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'should be able to close via url' do
+ data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'should allow multiple matches per line' do
+ message = <<-EOF
+ minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
+ ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
+ EOF
+ data = create_data_for_commits(message)
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
+
+ d2 = double('Asana::Task')
+ expect(d2).to receive(:add_comment)
+ expect(d2).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
+
+ d3 = double('Asana::Task')
+ expect(d3).to receive(:add_comment)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
+
+ d4 = double('Asana::Task')
+ expect(d4).to receive(:add_comment)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
+
+ d5 = double('Asana::Task')
+ expect(d5).to receive(:add_comment)
+ expect(d5).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
- @asana.check_commit('fix #456789', 'pushed')
+ @asana.execute(data)
end
end
end
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 230807ea672..88cd624877a 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe BuildkiteService do
+describe BuildkiteService, models: true do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
index e9967f5fe0b..a2cf68a9e38 100644
--- a/spec/models/project_services/drone_ci_service_spec.rb
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe DroneCiService do
+describe DroneCiService, models: true do
describe 'associations' do
it { is_expected.to belong_to(:project) }
it { is_expected.to have_one(:service_hook) }
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index 16296607a94..ff7fbcaa004 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe FlowdockService do
+describe FlowdockService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index 9e156472316..ecb3ccb1673 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe GemnasiumService do
+describe GemnasiumService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb
deleted file mode 100644
index 842089ebe0d..00000000000
--- a/spec/models/project_services/gitlab_ci_service_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-# == 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 GitlabCiService do
- describe 'associations' do
- it { is_expected.to belong_to(:project) }
- it { is_expected.to have_one(:service_hook) }
- end
-
- describe 'commits methods' do
- before do
- @ci_project = create(:ci_project)
- @service = GitlabCiService.new
- allow(@service).to receive_messages(
- service_hook: true,
- project_url: 'http://ci.gitlab.org/projects/2',
- token: 'verySecret',
- project: @ci_project.gl_project
- )
- end
-
- describe :build_page do
- it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/ci")}
- end
-
- describe "execute" do
- let(:user) { create(:user, username: 'username') }
- let(:project) { create(:project, name: 'project') }
- let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
-
- it "calls CreateCommitService" do
- expect_any_instance_of(Ci::CreateCommitService).to receive(:execute).with(@ci_project, user, push_sample_data)
-
- @service.execute(push_sample_data)
- end
- end
- end
-end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index e34ca09bffc..3518dbd1728 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe GitlabIssueTrackerService do
+describe GitlabIssueTrackerService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index f67d7b30980..91dd92b7c67 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe HipchatService do
+describe HipchatService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -57,23 +57,21 @@ describe HipchatService do
it 'should use v1 if version is provided' do
allow(hipchat).to receive(:api_version).and_return('v1')
- expect(HipChat::Client).to receive(:new).
- with(token,
- api_version: 'v1',
- server_url: server_url).
- and_return(
- double(:hipchat_service).as_null_object)
+ expect(HipChat::Client).to receive(:new).with(
+ token,
+ api_version: 'v1',
+ server_url: server_url
+ ).and_return(double(:hipchat_service).as_null_object)
hipchat.execute(push_sample_data)
end
it 'should use v2 as the version when nothing is provided' do
allow(hipchat).to receive(:api_version).and_return('')
- expect(HipChat::Client).to receive(:new).
- with(token,
- api_version: 'v2',
- server_url: server_url).
- and_return(
- double(:hipchat_service).as_null_object)
+ expect(HipChat::Client).to receive(:new).with(
+ token,
+ api_version: 'v2',
+ server_url: server_url
+ ).and_return(double(:hipchat_service).as_null_object)
hipchat.execute(push_sample_data)
end
@@ -247,6 +245,55 @@ describe HipchatService do
end
end
+ context 'build events' do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::BuildDataBuilder.build(build) }
+
+ context 'for failed' do
+ before { build.drop }
+
+ it "should call Hipchat API" do
+ hipchat.execute(data)
+
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ it "should create a build message" do
+ message = hipchat.send(:create_build_message, data)
+
+ project_url = project.web_url
+ project_name = project.name_with_namespace.gsub(/\s/, '')
+ sha = data[:sha]
+ ref = data[:ref]
+ ref_type = data[:tag] ? 'tag' : 'branch'
+ duration = data[:commit][:duration]
+
+ expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
+ "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+ "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
+ "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+ end
+ end
+
+ context 'for succeeded' do
+ before do
+ build.success
+ end
+
+ it "should call Hipchat API" do
+ hipchat.notify_only_broken_builds = false
+ hipchat.execute(data)
+ expect(WebMock).to have_requested(:post, api_url).once
+ end
+
+ 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
+ end
+ end
+ end
+
context "#message_options" do
it "should be set to the defaults" do
expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' })
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index 7d483a44c53..b783b1a576e 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -22,7 +22,7 @@ require 'spec_helper'
require 'socket'
require 'json'
-describe IrkerService do
+describe IrkerService, models: true do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index ddd2cce212c..2f8193170ae 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -20,12 +20,119 @@
require 'spec_helper'
-describe JiraService do
+describe JiraService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
end
+ describe "Execute" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request) }
+
+ before do
+ @jira_service = JiraService.new
+ allow(@jira_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ project_url: 'http://jira.example.com',
+ username: 'gitlab_jira_username',
+ password: 'gitlab_jira_password'
+ )
+ @jira_service.save # will build API URL, as api_url was not specified above
+ @sample_data = Gitlab::PushDataBuilder.build_sample(project, user)
+ # https://github.com/bblimke/webmock#request-with-basic-authentication
+ @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
+ @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
+
+ WebMock.stub_request(:post, @api_url)
+ WebMock.stub_request(:post, @comment_url)
+ end
+
+ it "should call JIRA API" do
+ @jira_service.execute(merge_request, JiraIssue.new("JIRA-123", project))
+ expect(WebMock).to have_requested(:post, @comment_url).with(
+ body: /Issue solved with/
+ ).once
+ end
+
+ 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))
+ expect(WebMock).to have_requested(:post, @api_url).with(
+ body: /this-is-a-custom-id/
+ ).once
+ end
+ end
+
+ describe "Stored password invalidation" do
+ let(:project) { create(:project) }
+
+ context "when a password was previously set" do
+ before do
+ @jira_service = JiraService.create(
+ project: create(:project),
+ properties: {
+ api_url: 'http://jira.example.com/rest/api/2',
+ username: 'mic',
+ password: "password"
+ }
+ )
+ end
+
+ it "reset password if url changed" do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.save
+ expect(@jira_service.password).to be_nil
+ end
+
+ it "does not reset password if username changed" do
+ @jira_service.username = "some_name"
+ @jira_service.save
+ expect(@jira_service.password).to eq("password")
+ end
+
+ it "does not reset password if new url is set together with password, even if it's the same password" do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.password = 'password'
+ @jira_service.save
+ expect(@jira_service.password).to eq("password")
+ expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
+ end
+
+ it "should reset password if url changed, even if setter called multiple times" do
+ @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
+ @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
+ @jira_service.save
+ expect(@jira_service.password).to be_nil
+ end
+ end
+
+ context "when no password was previously set" do
+ before do
+ @jira_service = JiraService.create(
+ project: create(:project),
+ properties: {
+ api_url: 'http://jira.example.com/rest/api/2',
+ username: 'mic'
+ }
+ )
+ end
+
+ it "saves password if new url is set together with password" do
+ @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.password = 'password'
+ @jira_service.save
+ 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
@@ -78,11 +185,12 @@ describe JiraService do
context 'when gitlab.yml was initialized' do
before do
- settings = { "jira" => {
- "title" => "Jira",
- "project_url" => "http://jira.sample/projects/project_a",
- "issues_url" => "http://jira.sample/issues/:id",
- "new_issue_url" => "http://jira.sample/projects/project_a/issues/new"
+ settings = {
+ "jira" => {
+ "title" => "Jira",
+ "project_url" => "http://jira.sample/projects/project_a",
+ "issues_url" => "http://jira.sample/issues/:id",
+ "new_issue_url" => "http://jira.sample/projects/project_a/issues/new"
}
}
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
@@ -94,9 +202,9 @@ describe JiraService do
end
it 'should be prepopulated with the settings' do
- expect(@service.properties[:project_url]).to eq('http://jira.sample/projects/project_a')
- expect(@service.properties[:issues_url]).to eq("http://jira.sample/issues/:id")
- expect(@service.properties[:new_issue_url]).to eq("http://jira.sample/projects/project_a/issues/new")
+ expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
+ expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
+ expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
end
end
end
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index ac10ffbd39b..96039f9491b 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe PushoverService do
+describe PushoverService, models: true do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
new file mode 100644
index 00000000000..621c83c0cda
--- /dev/null
+++ b/spec/models/project_services/slack_service/build_message_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe SlackService::BuildMessage do
+ subject { SlackService::BuildMessage.new(args) }
+
+ let(:args) do
+ {
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ ref: 'develop',
+ tag: false,
+
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+
+ commit: {
+ status: status,
+ author_name: 'hacker',
+ duration: 10,
+ },
+ }
+ end
+
+ context 'succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+
+ 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)'
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ context 'failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+
+ 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)'
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+end
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index b78d92f23a1..97e6f03e308 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SlackService::IssueMessage do
+describe SlackService::IssueMessage, models: true do
subject { SlackService::IssueMessage.new(args) }
let(:args) do
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
index 581c50d6c88..dae8bd90922 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/slack_service/merge_message_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SlackService::MergeMessage do
+describe SlackService::MergeMessage, models: true do
subject { SlackService::MergeMessage.new(args) }
let(:args) do
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 21fb575480b..06006b9a4f5 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SlackService::NoteMessage do
+describe SlackService::NoteMessage, models: true do
let(:color) { '#345' }
before do
@@ -89,10 +89,10 @@ describe SlackService::NoteMessage do
it 'returns a message regarding notes on an issue' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq(
- "Test User commented on " \
- "<url|issue #20> in <somewhere.com|project_name>: " \
- "*issue title*")
- expected_attachments = [
+ "Test User commented on " \
+ "<url|issue #20> in <somewhere.com|project_name>: " \
+ "*issue title*")
+ expected_attachments = [
{
text: "comment on an issue",
color: color,
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb
index ddc290820d1..cda9ee670b0 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/slack_service/push_message_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SlackService::PushMessage do
+describe SlackService::PushMessage, models: true do
subject { SlackService::PushMessage.new(args) }
let(:args) do
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 97b60e19e40..a9e0afad90f 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe SlackService do
+describe SlackService, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb
index 3e8f106d27f..cc92eb0bd9f 100644
--- a/spec/models/project_snippet_spec.rb
+++ b/spec/models/project_snippet_spec.rb
@@ -17,7 +17,7 @@
require 'spec_helper'
-describe ProjectSnippet do
+describe ProjectSnippet, models: true do
describe "Associations" do
it { is_expected.to belong_to(:project) }
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f93935ebe3b..400bdf2d962 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -28,11 +28,12 @@
# import_type :string(255)
# import_source :string(255)
# commit_count :integer default(0)
+# import_error :text
#
require 'spec_helper'
-describe Project do
+describe Project, models: true do
describe 'associations' do
it { is_expected.to belong_to(:group) }
it { is_expected.to belong_to(:namespace) }
@@ -53,6 +54,13 @@ describe Project do
it { is_expected.to have_one(:slack_service).dependent(:destroy) }
it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
it { is_expected.to have_one(:asana_service).dependent(:destroy) }
+ it { is_expected.to have_many(:commit_statuses) }
+ it { is_expected.to have_many(:ci_commits) }
+ 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) }
end
describe 'modules' do
@@ -87,6 +95,18 @@ describe Project do
expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/)
end
end
+
+ describe 'project token' do
+ it 'should set an random token if none provided' do
+ project = FactoryGirl.create :empty_project, runners_token: ''
+ expect(project.runners_token).not_to eq('')
+ end
+
+ it 'should not set an random toke if one provided' do
+ project = FactoryGirl.create :empty_project, runners_token: 'my-token'
+ expect(project.runners_token).to eq('my-token')
+ end
+ end
describe 'Respond to' do
it { is_expected.to respond_to(:url_to_repo) }
@@ -152,13 +172,17 @@ describe Project do
describe '#get_issue' do
let(:project) { create(:empty_project) }
- let(:issue) { create(:issue, project: project) }
+ let!(:issue) { create(:issue, project: project) }
context 'with default issues tracker' do
it 'returns an issue' do
expect(project.get_issue(issue.iid)).to eq issue
end
+ it 'returns count of open issues' do
+ expect(project.open_issues_count).to eq(1)
+ end
+
it 'returns nil when no issue found' do
expect(project.get_issue(999)).to be_nil
end
@@ -345,17 +369,6 @@ describe Project do
expect(project1.star_count).to eq(0)
expect(project2.star_count).to eq(0)
end
-
- it 'is decremented when an upvoter account is deleted' do
- user = create :user
- project = create :project, :public
- user.toggle_star(project)
- project.reload
- expect(project.star_count).to eq(1)
- user.destroy
- project.reload
- expect(project.star_count).to eq(0)
- end
end
describe :avatar_type do
@@ -405,23 +418,19 @@ describe Project do
describe :ci_commit do
let(:project) { create :project }
- let(:commit) { create :ci_commit, gl_project: project }
-
- before do
- project.ensure_gitlab_ci_project
- project.create_gitlab_ci_service(active: true)
- end
+ let(:commit) { create :ci_commit, project: project }
it { expect(project.ci_commit(commit.sha)).to eq(commit) }
end
- describe :enable_ci do
+ describe :builds_enabled do
let(:project) { create :project }
- before { project.enable_ci }
+ before { project.builds_enabled = true }
+
+ subject { project.builds_enabled }
- it { expect(project.gitlab_ci?).to be_truthy }
- it { expect(project.gitlab_ci_project).to be_a(Ci::Project) }
+ it { expect(project.builds_enabled?).to be_truthy }
end
describe '.trending' do
@@ -452,7 +461,9 @@ describe Project do
before do
2.times do
- create(:note_on_commit, project: project2, created_at: date)
+ # Little fix for special issue related to Fractional Seconds support for MySQL.
+ # See: https://github.com/rails/rails/pull/14359/files
+ create(:note_on_commit, project: project2, created_at: date + 1)
end
end
@@ -461,4 +472,108 @@ describe Project do
end
end
end
+
+ describe '.visible_to_user' do
+ let!(:project) { create(:project, :private) }
+ let!(:user) { create(:user) }
+
+ subject { described_class.visible_to_user(user) }
+
+ describe 'when a user has access to a project' do
+ before do
+ project.team.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ it { is_expected.to eq([project]) }
+ end
+
+ describe 'when a user does not have access to any projects' do
+ it { is_expected.to eq([]) }
+ end
+ end
+
+ context 'shared runners by default' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.shared_runners_enabled }
+
+ context 'are enabled' do
+ before { stub_application_setting(shared_runners_enabled: true) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'are disabled' do
+ before { stub_application_setting(shared_runners_enabled: false) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe :any_runners do
+ let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) }
+ let(:specific_runner) { create(:ci_specific_runner) }
+ let(:shared_runner) { create(:ci_shared_runner) }
+
+ context 'for shared runners disabled' do
+ let(:shared_runners_enabled) { false }
+
+ it 'there are no runners available' do
+ expect(project.any_runners?).to be_falsey
+ end
+
+ it 'there is a specific runner' do
+ project.runners << specific_runner
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it 'there is a shared runner, but they are prohibited to use' do
+ shared_runner
+ expect(project.any_runners?).to be_falsey
+ end
+
+ it 'checks the presence of specific runner' do
+ project.runners << specific_runner
+ expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy
+ end
+ end
+
+ context 'for shared runners enabled' do
+ let(:shared_runners_enabled) { true }
+
+ it 'there is a shared runner' do
+ shared_runner
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it 'checks the presence of shared runner' do
+ shared_runner
+ expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy
+ end
+ end
+ end
+
+ describe '#visibility_level_allowed?' do
+ let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
+
+ context 'when checking on non-forked project' do
+ it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
+ it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
+ it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_truthy }
+ end
+
+ context 'when checking on forked project' do
+ let(:forked_project) { create :forked_project_with_submodules }
+
+ before do
+ forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
+ forked_project.save
+ end
+
+ it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy }
+ it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy }
+ it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey }
+ end
+
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 26e8fdae472..5cd5ae327bf 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe ProjectTeam do
+describe ProjectTeam, models: true do
let(:master) { create(:user) }
let(:reporter) { create(:user) }
let(:guest) { create(:user) }
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 94802dcfb79..876b927eaea 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe ProjectWiki do
+describe ProjectWiki, models: true do
let(:project) { create(:empty_project) }
let(:repository) { project.repository }
let(:user) { project.owner }
@@ -184,6 +184,12 @@ describe ProjectWiki do
subject.create_page("test page", "some content", :markdown, "commit message")
expect(subject.pages.first.page.version.message).to eq("commit message")
end
+
+ it 'updates project activity' do
+ expect(subject).to receive(:update_project_activity)
+
+ subject.create_page('Test Page', 'This is content')
+ end
end
describe "#update_page" do
@@ -205,6 +211,12 @@ describe ProjectWiki do
it "sets the correct commit message" do
expect(@page.version.message).to eq("updated page")
end
+
+ it 'updates project activity' do
+ expect(subject).to receive(:update_project_activity)
+
+ subject.update_page(@gollum_page, 'Yet more content', :markdown, 'Updated page again')
+ end
end
describe "#delete_page" do
@@ -217,13 +229,19 @@ describe ProjectWiki do
subject.delete_page(@page)
expect(subject.pages.count).to eq(0)
end
+
+ it 'updates project activity' do
+ expect(subject).to receive(:update_project_activity)
+
+ subject.delete_page(@page)
+ end
end
private
def create_temp_repo(path)
FileUtils.mkdir_p path
- system(*%W(git init --quiet --bare -- #{path}))
+ system(*%W(#{Gitlab.config.git.bin_path} init --quiet --bare -- #{path}))
end
def remove_temp_repo(path)
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index 1e6937b536c..7e956cf6779 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -12,7 +12,7 @@
require 'spec_helper'
-describe ProtectedBranch do
+describe ProtectedBranch, models: true do
describe 'Associations' do
it { is_expected.to belong_to(:project) }
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
new file mode 100644
index 00000000000..72ecb442a36
--- /dev/null
+++ b/spec/models/release_spec.rb
@@ -0,0 +1,28 @@
+# == Schema Information
+#
+# Table name: releases
+#
+# id :integer not null, primary key
+# tag :string(255)
+# description :text
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'rails_helper'
+
+RSpec.describe Release, type: :model do
+ let(:release) { create(:release) }
+
+ it { expect(release).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validation' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:description) }
+ end
+end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 05e51532eb8..afbf62035ac 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
-describe Repository do
+describe Repository, models: true do
include RepoHelpers
let(:repository) { create(:project).repository }
+ let(:user) { create(:user) }
describe :branch_names_contains do
subject { repository.branch_names_contains(sample_commit.id) }
@@ -26,6 +27,15 @@ describe Repository do
it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') }
end
+ describe :find_commits_by_message do
+ subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
+
+ it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
+ it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') }
+ it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ end
+
describe :blob_at do
context 'blank sha' do
subject { repository.blob_at(Gitlab::Git::BLANK_SHA, '.gitignore') }
@@ -90,5 +100,123 @@ describe Repository do
it { expect(subject.startline).to eq(186) }
it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") }
end
+
+ end
+
+ describe "#license" do
+ before do
+ repository.send(:cache).expire(:license)
+ TestBlob = Struct.new(:name)
+ end
+
+ it 'test selection preference' do
+ files = [TestBlob.new('file'), TestBlob.new('license'), TestBlob.new('copying')]
+ expect(repository.tree).to receive(:blobs).and_return(files)
+
+ expect(repository.license.name).to eq('license')
+ end
+
+ it 'also accepts licence instead of license' do
+ expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('licence')])
+
+ expect(repository.license.name).to eq('licence')
+ end
+ end
+
+ describe :add_branch do
+ context 'when pre hooks were successful' do
+ it 'should run without errors' do
+ hook = double(trigger: true)
+ 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)
+
+ branch = repository.add_branch(user, 'new_feature', 'master')
+
+ expect(branch.name).to eq('new_feature')
+ end
+ end
+
+ 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)
+
+ expect do
+ repository.add_branch(user, 'new_feature', 'master')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+
+ it 'should not create the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.add_branch(user, 'new_feature', 'master')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ expect(repository.find_branch('new_feature')).to be_nil
+ end
+ end
+ end
+
+ 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)
+
+ 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)
+
+ expect { repository.rm_branch(user, 'feature') }.not_to raise_error
+
+ expect(repository.find_branch('feature')).to be_nil
+ end
+ end
+
+ 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)
+
+ expect do
+ repository.rm_branch(user, 'new_feature')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+
+ it 'should not delete the branch' do
+ allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return(false)
+
+ expect do
+ repository.rm_branch(user, 'feature')
+ end.to raise_error(GitHooksService::PreReceiveError)
+ expect(repository.find_branch('feature')).not_to be_nil
+ end
+ end
+ end
+
+ describe :commit_with_hooks do
+ context 'when pre hooks were successful' do
+ it 'should run without errors' do
+ expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true)
+
+ expect do
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ end.not_to raise_error
+ end
+ end
+
+ 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)
+
+ expect do
+ repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+ end
end
end
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 692e5fda3ba..0ca82365b98 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -20,7 +20,7 @@
require 'spec_helper'
-describe Service do
+describe Service, models: true do
describe "Associations" do
it { is_expected.to belong_to :project }
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index 81581838675..eb2dbbdc5a4 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -17,7 +17,7 @@
require 'spec_helper'
-describe Snippet do
+describe Snippet, models: true do
describe 'modules' do
subject { described_class }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index c71cfb3ebe3..a16161e673e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -26,6 +26,7 @@
# bio :string(255)
# failed_attempts :integer default(0)
# locked_at :datetime
+# unlock_token :string(255)
# username :string(255)
# can_create_group :boolean default(TRUE), not null
# can_create_team :boolean default(TRUE), not null
@@ -54,11 +55,14 @@
# public_email :string(255) default(""), not null
# dashboard :integer default(0)
# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
#
require 'spec_helper'
-describe User do
+describe User, models: true do
include Gitlab::CurrentSettings
describe 'modules' do
@@ -89,7 +93,23 @@ describe User do
end
describe 'validations' do
- it { is_expected.to validate_presence_of(:username) }
+ describe 'username' do
+ it 'validates presence' do
+ expect(subject).to validate_presence_of(:username)
+ end
+
+ it 'rejects blacklisted names' do
+ user = build(:user, username: 'dashboard')
+
+ expect(user).not_to be_valid
+ expect(user.errors.values).to eq [['dashboard is a reserved name']]
+ end
+
+ it 'validates uniqueness' do
+ expect(subject).to validate_uniqueness_of(:username).case_insensitive
+ end
+ end
+
it { is_expected.to validate_presence_of(:projects_limit) }
it { is_expected.to validate_numericality_of(:projects_limit) }
it { is_expected.to allow_value(0).for(:projects_limit) }
@@ -443,8 +463,8 @@ describe User do
expect(User.search(user1.username.downcase).to_a).to eq([user1])
expect(User.search(user2.username.upcase).to_a).to eq([user2])
expect(User.search(user2.username.downcase).to_a).to eq([user2])
- expect(User.search(user1.username.downcase).to_a.count).to eq(2)
- expect(User.search(user2.username.downcase).to_a.count).to eq(1)
+ expect(User.search(user1.username.downcase).to_a.size).to eq(2)
+ expect(User.search(user2.username.downcase).to_a.size).to eq(1)
end
end
@@ -663,28 +683,28 @@ describe User do
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
end
- it "sorts users as recently_signed_in" do
+ it "sorts users by the recent sign-in time" do
expect(User.sort('recent_sign_in').first).to eq(@user)
end
- it "sorts users as late_signed_in" do
+ it "sorts users by the oldest sign-in time" do
expect(User.sort('oldest_sign_in').first).to eq(@user1)
end
- it "sorts users as recently_created" do
+ it "sorts users in descending order by their creation time" do
expect(User.sort('created_desc').first).to eq(@user)
end
- it "sorts users as late_created" do
+ it "sorts users in ascending order by their creation time" do
expect(User.sort('created_asc').first).to eq(@user1)
end
- it "sorts users by name when nil is passed" do
- expect(User.sort(nil).first).to eq(@user)
+ it "sorts users by id in descending order when nil is passed" do
+ expect(User.sort(nil).first).to eq(@user1)
end
end
- describe "#contributed_projects_ids" do
+ describe "#contributed_projects" do
subject { create(:user) }
let!(:project1) { create(:project) }
let!(:project2) { create(:project, forked_from_project: project3) }
@@ -699,15 +719,15 @@ describe User do
end
it "includes IDs for projects the user has pushed to" do
- expect(subject.contributed_projects_ids).to include(project1.id)
+ expect(subject.contributed_projects).to include(project1)
end
it "includes IDs for projects the user has had merge requests merged into" do
- expect(subject.contributed_projects_ids).to include(project3.id)
+ expect(subject.contributed_projects).to include(project3)
end
it "doesn't include IDs for unrelated projects" do
- expect(subject.contributed_projects_ids).not_to include(project2.id)
+ expect(subject.contributed_projects).not_to include(project2)
end
end
@@ -756,4 +776,30 @@ describe User do
expect(subject.recent_push).to eq(nil)
end
end
+
+ describe '#authorized_groups' do
+ let!(:user) { create(:user) }
+ let!(:private_group) { create(:group) }
+
+ before do
+ private_group.add_user(user, Gitlab::Access::MASTER)
+ end
+
+ subject { user.authorized_groups }
+
+ it { is_expected.to eq([private_group]) }
+ end
+
+ describe '#authorized_projects' do
+ let!(:user) { create(:user) }
+ let!(:private_project) { create(:project, :private) }
+
+ before do
+ private_project.team << [user, Gitlab::Access::MASTER]
+ end
+
+ subject { user.authorized_projects }
+
+ it { is_expected.to eq([private_project]) }
+ end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index d7802d1734f..c1b03838aa9 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -1,6 +1,6 @@
require "spec_helper"
-describe WikiPage do
+describe WikiPage, models: true do
let(:project) { create(:empty_project) }
let(:user) { project.owner }
let(:wiki) { ProjectWiki.new(project, user) }
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 4048c297013..0c19094ec54 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe API, api: true do
- include API::APIHelpers
+ include API::Helpers
include ApiHelpers
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -13,25 +13,25 @@ describe API, api: true do
def set_env(token_usr, identifier)
clear_env
clear_param
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
- env[API::APIHelpers::SUDO_HEADER] = identifier
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = token_usr.private_token
+ env[API::Helpers::SUDO_HEADER] = identifier
end
def set_param(token_usr, identifier)
clear_env
clear_param
- params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
- params[API::APIHelpers::SUDO_PARAM] = identifier
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = token_usr.private_token
+ params[API::Helpers::SUDO_PARAM] = identifier
end
def clear_env
- env.delete(API::APIHelpers::PRIVATE_TOKEN_HEADER)
- env.delete(API::APIHelpers::SUDO_HEADER)
+ env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
+ env.delete(API::Helpers::SUDO_HEADER)
end
def clear_param
- params.delete(API::APIHelpers::PRIVATE_TOKEN_PARAM)
- params.delete(API::APIHelpers::SUDO_PARAM)
+ params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
+ params.delete(API::Helpers::SUDO_PARAM)
end
def error!(message, status)
@@ -40,22 +40,22 @@ describe API, api: true do
describe ".current_user" do
it "should return nil for an invalid token" do
- env[API::APIHelpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+ 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::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ 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::APIHelpers::PRIVATE_TOKEN_HEADER] = user.private_token
+ env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
expect(current_user).to eq(user)
clear_env
- params[API::APIHelpers::PRIVATE_TOKEN_PARAM] = user.private_token
+ params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
expect(current_user).to eq(user)
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 5c1b58535cc..36461e84c3a 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -118,7 +118,7 @@ describe API::API, api: true do
branch_name: 'new design',
ref: branch_sha
expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Branch name invalid')
+ expect(json_response['message']).to eq('Branch name is invalid')
end
it 'should return 400 if branch already exists' do
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
index b9e6dfc15a7..a28607bd240 100644
--- a/spec/requests/api/commit_status_spec.rb
+++ b/spec/requests/api/commit_status_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
before do
@status1 = create(:commit_status, commit: ci_commit, status: 'running')
@status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending')
- @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running')
+ @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true)
@status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success')
@status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success')
@status6 = create(:commit_status, commit: ci_commit, status: 'success')
@@ -30,6 +30,8 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id)
+ json_response.sort_by!{ |status| status['id'] }
+ expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false])
end
it "should return all commit statuses" do
diff --git a/spec/requests/api/files_spec.rb b/spec/requests/api/files_spec.rb
index 042e6352567..8efa09f75fd 100644
--- a/spec/requests/api/files_spec.rb
+++ b/spec/requests/api/files_spec.rb
@@ -19,6 +19,7 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response['file_path']).to eq(file_path)
expect(json_response['file_name']).to eq('popen.rb')
+ expect(json_response['last_commit_id']).to eq('570e7b2abdd848b95f2f578043fc23bd6f6fd24d')
expect(Base64.decode64(json_response['content']).lines.first).to eq("require 'fileutils'\n")
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 13cced81875..4cfa49d1566 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -10,6 +10,8 @@ describe API::API, api: true do
let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
let!(:group2) { create(:group) }
+ let!(:project1) { create(:project, namespace: group1) }
+ let!(:project2) { create(:project, namespace: group2) }
before do
group1.add_owner(user1)
@@ -67,7 +69,7 @@ describe API::API, api: true do
it "should return any existing group" do
get api("/groups/#{group2.id}", admin)
expect(response.status).to eq(200)
- json_response['name'] == group2.name
+ expect(json_response['name']).to eq(group2.name)
end
it "should not return a non existing group" do
@@ -80,7 +82,7 @@ describe API::API, api: true do
it 'should return any existing group' do
get api("/groups/#{group1.path}", admin)
expect(response.status).to eq(200)
- json_response['name'] == group2.name
+ expect(json_response['name']).to eq(group1.name)
end
it 'should not return a non existing group' do
@@ -95,6 +97,59 @@ describe API::API, api: true do
end
end
+ describe "GET /groups/:id/projects" 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)
+ end
+
+ it "should not return a non existing group" do
+ get api("/groups/1328/projects", user1)
+ expect(response.status).to eq(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(403)
+ 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(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)
+ 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)
+ end
+
+ it 'should not return a non existing group' do
+ get api('/groups/unknown/projects', admin)
+ expect(response.status).to eq(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(403)
+ end
+ end
+ end
+
describe "POST /groups" do
context "when authenticated as user without group permissions" do
it "should not create group" do
diff --git a/spec/requests/api/labels_spec.rb b/spec/requests/api/labels_spec.rb
index aff109a9424..667f0dbea5c 100644
--- a/spec/requests/api/labels_spec.rb
+++ b/spec/requests/api/labels_spec.rb
@@ -47,7 +47,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAA'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for too long color code' do
@@ -55,7 +55,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAAFFFF'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for invalid name' do
@@ -151,12 +151,12 @@ describe API::API, api: true do
expect(json_response['message']['title']).to eq(['is invalid'])
end
- it 'should return 400 for invalid name' do
+ it 'should return 400 when color code is too short' do
put api("/projects/#{project.id}/labels", user),
name: 'label1',
color: '#FF'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
it 'should return 400 for too long color code' do
@@ -164,7 +164,7 @@ describe API::API, api: true do
name: 'Foo',
color: '#FFAAFFFF'
expect(response.status).to eq(400)
- expect(json_response['message']['color']).to eq(['is invalid'])
+ expect(json_response['message']['color']).to eq(['must be a valid color code'])
end
end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index a68c7b1e461..e194eb93cf4 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -6,7 +6,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.seconds) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
@@ -131,6 +131,23 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) }
+ let(:commit) { merge_request.commits.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response.size).to eq(merge_request.commits.size) }
+ it { expect(json_response.first['id']).to eq(commit.id) }
+ it { expect(json_response.first['title']).to eq(commit.title) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_request/999/commits", user)
+ expect(response.status).to eq(404)
+ end
+ end
+
describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do
it 'should return the change information of the merge_request' do
get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user)
@@ -303,19 +320,21 @@ describe API::API, api: true do
end
describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do
+ let(:ci_commit) { create(:ci_commit_without_jobs) }
+
it "should return merge_request in case of success" do
put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
expect(response.status).to eq(200)
end
- it "should return 405 if branch can't be merged" do
+ it "should return 406 if branch can't be merged" do
allow_any_instance_of(MergeRequest).
to receive(:can_be_merged?).and_return(false)
put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
- expect(response.status).to eq(405)
+ expect(response.status).to eq(406)
expect(json_response['message']).to eq('Branch cannot be merged')
end
@@ -340,6 +359,17 @@ describe API::API, api: true do
expect(response.status).to eq(401)
expect(json_response['message']).to eq('401 Unauthorized')
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)
+
+ put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true
+
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq('Test')
+ expect(json_response['merge_when_build_succeeds']).to eq(true)
+ end
end
describe "PUT /projects/:id/merge_request/:merge_request_id" do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 606b226ad77..142b637d291 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -1,11 +1,17 @@
require 'spec_helper'
-describe API::API, 'ProjectHooks', api: true do
+describe API::API, 'ProjectHooks', api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:hook) { create(:project_hook, project: project, url: "http://example.com", push_events: true, merge_requests_events: true, tag_push_events: true, issues_events: true, note_events: true, enable_ssl_verification: true) }
+ let!(:hook) do
+ create(:project_hook,
+ project: project, url: "http://example.com",
+ push_events: true, merge_requests_events: true, tag_push_events: true,
+ issues_events: true, note_events: true, build_events: true,
+ enable_ssl_verification: true)
+ end
before do
project.team << [user, :master]
@@ -26,6 +32,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
+ expect(json_response.first['build_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
@@ -83,6 +90,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
+ expect(json_response['build_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index e9de9e0826d..ab2530859ea 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -65,6 +65,22 @@ describe API::API, api: true do
expect(json_response.first.keys).to include('tag_list')
end
+ it 'should include open_issues_count' do
+ get api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('open_issues_count')
+ end
+
+ it 'should not include open_issues_count' do
+ project.update_attributes( { issues_enabled: false } )
+
+ get api('/projects', user)
+ expect(response.status).to eq 200
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).not_to include('open_issues_count')
+ end
+
context 'and using search' do
it 'should return searched project' do
get api('/projects', user), { search: project.name }
@@ -86,15 +102,6 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.first['id']).to eq(project3.id)
end
-
- it 'returns projects in the correct order when ci_enabled_first parameter is passed' do
- [project, project2, project3].each{ |project| project.build_missing_services }
- project2.gitlab_ci_service.update(active: true)
- get api('/projects', user), { ci_enabled_first: 'true' }
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.first['id']).to eq(project2.id)
- end
end
end
end
@@ -124,6 +131,7 @@ describe API::API, api: true do
expect(json_response).to satisfy do |response|
response.one? do |entry|
+ entry.has_key?('permissions') &&
entry['name'] == project.name &&
entry['owner']['username'] == user.username
end
@@ -132,6 +140,25 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/starred' do
+ before do
+ admin.starred_projects << project
+ admin.save!
+ end
+
+ it 'should return the starred projects' do
+ get api('/projects/all', admin)
+ expect(response.status).to eq(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
+ end
+ end
+
describe 'POST /projects' do
context 'maximum number of projects reached' do
it 'should not create new project and respond with 403' do
@@ -355,7 +382,28 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it 'should handle users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
describe 'permissions' do
+ context 'all projects' do
+ it 'Contains permission information' do
+ project.team << [user, :master]
+ get api("/projects", user)
+
+ expect(response.status).to eq(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
+ end
+ end
+
context 'personal project' do
it 'Sets project access and returns 200' do
project.team << [user, :master]
@@ -386,14 +434,30 @@ describe API::API, api: true do
describe 'GET /projects/:id/events' do
before { project_member2 }
- it 'should return a project events' do
- get api("/projects/#{project.id}/events", user)
- expect(response.status).to eq(200)
- json_event = json_response.first
+ context 'valid request' do
+ before do
+ note = create(:note_on_issue, note: 'What an awesome day!', project: project)
+ EventCreateService.new.leave_note(note, note.author)
+ get api("/projects/#{project.id}/events", user)
+ end
+
+ it { expect(response.status).to eq(200) }
- expect(json_event['action_name']).to eq('joined')
- expect(json_event['project_id'].to_i).to eq(project.id)
- expect(json_event['author_username']).to eq(user3.username)
+ context 'joined event' do
+ let(:json_event) { json_response[1] }
+
+ it { expect(json_event['action_name']).to eq('joined') }
+ it { expect(json_event['project_id'].to_i).to eq(project.id) }
+ it { expect(json_event['author_username']).to eq(user3.username) }
+ it { expect(json_event['author']['name']).to eq(user3.name) }
+ end
+
+ context 'comment event' do
+ let(:json_event) { json_response.first }
+
+ it { expect(json_event['action_name']).to eq('commented on') }
+ it { expect(json_event['note']['body']).to eq('What an awesome day!') }
+ end
end
it 'should return a 404 error if not found' do
@@ -448,7 +512,7 @@ describe API::API, api: true do
end
end
- describe 'PUT /projects/:id/snippets/:shippet_id' do
+ describe 'PUT /projects/:id/snippets/:snippet_id' do
it 'should update an existing project snippet' do
put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
code: 'updated code'
@@ -723,6 +787,18 @@ describe API::API, api: true do
end
end
+ it 'should update visibility_level from public to private' do
+ project3.update_attributes({ visibility_level: Gitlab::VisibilityLevel::PUBLIC })
+
+ project_param = { public: false }
+ put api("/projects/#{project3.id}", user), project_param
+ expect(response.status).to eq(200)
+ project_param.each_pair do |k, v|
+ expect(json_response[k.to_s]).to eq(v)
+ end
+ expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
it 'should not update name to existing name' do
project_param = { name: project3.name }
put api("/projects/#{project.id}", user), project_param
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 1149f7e7989..4911cdd9da6 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -11,81 +11,6 @@ describe API::API, api: true do
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
- describe "GET /projects/:id/repository/tags" 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(json_response).to be_an Array
- expect(json_response.first['name']).to eq(project.repo.tags.sort_by(&:name).reverse.first.name)
- end
- end
-
- describe 'POST /projects/:id/repository/tags' do
- context 'lightweight tags' do
- it 'should create a new tag' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.0.1',
- ref: 'master'
-
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq('v7.0.1')
- end
- end
-
- context 'annotated tag' do
- it 'should create a new annotated tag' do
- # Identity must be set in .gitconfig to create annotated tag.
- repo_path = project.repository.path_to_repo
- system(*%W(git --git-dir=#{repo_path} config user.name #{user.name}))
- system(*%W(git --git-dir=#{repo_path} config user.email #{user.email}))
-
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.1.0',
- ref: 'master',
- message: 'Release 7.1.0'
-
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq('v7.1.0')
- expect(json_response['message']).to eq('Release 7.1.0')
- end
- end
-
- it 'should deny for user without push access' do
- post api("/projects/#{project.id}/repository/tags", user2),
- tag_name: 'v1.9.0',
- ref: '621491c677087aa243f165eab467bfdfbee00be1'
- expect(response.status).to eq(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(json_response['message']).to eq('Tag name invalid')
- end
-
- it 'should return 400 if tag already exists' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response.status).to eq(201)
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Tag already exists')
- end
-
- it 'should return 400 if ref name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'mytag',
- ref: 'foo'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Invalid reference name')
- end
- end
-
describe "GET /projects/:id/repository/tree" do
context "authorized user" do
before { project.team << [user2, :reporter] }
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index c0226605a23..fed9ae1949b 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -29,7 +29,7 @@ describe API::API, api: true do
if required_attributes.empty?
expected_code = 200
else
- attrs.delete(required_attributes.shuffle.first)
+ attrs.delete(required_attributes.sample)
expected_code = 400
end
@@ -46,6 +46,7 @@ describe API::API, api: true do
delete api("/projects/#{project.id}/services/#{dashed_service}", user)
expect(response.status).to eq(200)
+ project.send(service_method).reload
expect(project.send(service_method).activated?).to be_falsey
end
end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
new file mode 100644
index 00000000000..17f2643fd45
--- /dev/null
+++ b/spec/requests/api/tags_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::API, api: true do
+ include ApiHelpers
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+ let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+
+ describe "GET /projects/:id/repository/tags" do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ let(:description) { 'Awesome release!' }
+
+ 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(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'with releases' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ 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(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['release']['description']).to eq(description)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/repository/tags' do
+ context 'lightweight tags' do
+ it 'should create a new tag' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.0.1',
+ ref: 'master'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.0.1')
+ end
+ end
+
+ context 'lightweight tags with release notes' do
+ it 'should create a new tag' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.0.1',
+ ref: 'master',
+ release_description: 'Wow'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.0.1')
+ expect(json_response['release']['description']).to eq('Wow')
+ end
+ end
+
+ context 'annotated tag' do
+ it 'should create a new annotated tag' do
+ # Identity must be set in .gitconfig to create annotated tag.
+ repo_path = project.repository.path_to_repo
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
+
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.1.0',
+ ref: 'master',
+ message: 'Release 7.1.0'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.1.0')
+ expect(json_response['message']).to eq('Release 7.1.0')
+ end
+ end
+
+ it 'should deny for user without push access' do
+ post api("/projects/#{project.id}/repository/tags", user2),
+ tag_name: 'v1.9.0',
+ ref: '621491c677087aa243f165eab467bfdfbee00be1'
+ expect(response.status).to eq(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(json_response['message']).to eq('Tag name invalid')
+ end
+
+ it 'should return 400 if tag already exists' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v8.0.0',
+ ref: 'master'
+ expect(response.status).to eq(201)
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v8.0.0',
+ ref: 'master'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Tag already exists')
+ end
+
+ it 'should return 400 if ref name is invalid' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'mytag',
+ ref: 'foo'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Invalid reference name')
+ end
+ end
+
+ describe 'POST /projects/:id/repository/tags/:tag_name/release' do
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+
+ it 'should create description for existing git tag' do
+ post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: description
+
+ expect(response.status).to eq(201)
+ expect(json_response['tag_name']).to eq(tag_name)
+ expect(json_response['description']).to eq(description)
+ end
+
+ it 'should return 404 if the tag does not exist' do
+ post api("/projects/#{project.id}/repository/tags/foobar/release", user),
+ description: description
+
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('Tag does not exist')
+ end
+
+ context 'on tag with existing release' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it 'should return 409 if there is already a release' do
+ post api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: description
+
+ expect(response.status).to eq(409)
+ expect(json_response['message']).to eq('Release already exists')
+ end
+ end
+ end
+
+ describe 'PUT id/repository/tags/:tag_name/release' do
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+ let(:new_description) { 'The best release!' }
+
+ context 'on tag with existing release' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ end
+
+ it 'should update the release description' do
+ put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: new_description
+
+ expect(response.status).to eq(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ expect(json_response['description']).to eq(new_description)
+ end
+ end
+
+ it 'should return 404 if the tag does not exist' do
+ put api("/projects/#{project.id}/repository/tags/foobar/release", user),
+ description: new_description
+
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('Tag does not exist')
+ end
+
+ it 'should return 404 if the release does not exist' do
+ put api("/projects/#{project.id}/repository/tags/#{tag_name}/release", user),
+ description: new_description
+
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('Release does not exist')
+ end
+ end
+end
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
new file mode 100644
index 00000000000..314bd7ddc59
--- /dev/null
+++ b/spec/requests/api/triggers_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe API::API do
+ include ApiHelpers
+
+ describe 'POST /projects/:project_id/trigger' do
+ let!(:trigger_token) { 'secure token' }
+ let!(:project) { FactoryGirl.create(:project) }
+ let!(:project2) { FactoryGirl.create(:empty_project) }
+ let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
+ let(:options) do
+ {
+ token: trigger_token
+ }
+ end
+
+ before do
+ stub_ci_commit_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)
+ 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)
+ 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)
+ end
+ end
+
+ context 'Have a commit' do
+ let(:commit) { project.ci_commits.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)
+ 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(json_response['message']).to eq('No builds created')
+ end
+
+ context 'Validates variables' do
+ let(:variables) do
+ { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
+ end
+
+ 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(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(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)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index d26a300ed82..4f278551d07 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -27,6 +27,13 @@ describe API::API, api: true do
user['username'] == username
end['username']).to eq(username)
end
+
+ it "should return one user" do
+ get api("/users?username=#{omniauth_user.username}", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['username']).to eq(omniauth_user.username)
+ end
end
context "when admin" do
@@ -153,7 +160,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.send(:namespace_regex_message)])
+ to eq([Gitlab::Regex.namespace_regex_message])
end
it "shouldn't available for non admin users" do
@@ -296,7 +303,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.send(:namespace_regex_message)])
+ to eq([Gitlab::Regex.namespace_regex_message])
end
context "with existing user" do
@@ -343,8 +350,9 @@ describe API::API, api: true do
end.to change{ user.keys.count }.by(1)
end
- it "should raise error for invalid ID" do
- expect{post api("/users/ASDF/keys", admin) }.to raise_error(ActionController::RoutingError)
+ it "should return 405 for invalid ID" do
+ post api("/users/ASDF/keys", admin)
+ expect(response.status).to eq(405)
end
end
@@ -374,9 +382,9 @@ describe API::API, api: true do
expect(json_response.first['title']).to eq(key.title)
end
- it "should return 404 for invalid ID" do
+ it "should return 405 for invalid ID" do
get api("/users/ASDF/keys", admin)
- expect(response.status).to eq(404)
+ expect(response.status).to eq(405)
end
end
end
@@ -434,7 +442,8 @@ describe API::API, api: true do
end
it "should raise error for invalid ID" do
- expect{post api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError)
+ post api("/users/ASDF/emails", admin)
+ expect(response.status).to eq(405)
end
end
@@ -465,7 +474,8 @@ describe API::API, api: true do
end
it "should raise error for invalid ID" do
- expect{put api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError)
+ put api("/users/ASDF/emails", admin)
+ expect(response.status).to eq(405)
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 88218a93e1f..c27e87c4acc 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -4,8 +4,7 @@ describe Ci::API::API do
include ApiHelpers
let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:project) { FactoryGirl.create(:empty_project) }
before do
stub_ci_commit_to_return_yaml_file
@@ -13,16 +12,15 @@ describe Ci::API::API do
describe "Builds API for runners" do
let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
- let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") }
- let(:shared_gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: shared_project) }
+ let(:shared_project) { FactoryGirl.create(:empty_project, name: "SharedProject") }
before do
- FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id
+ FactoryGirl.create :ci_runner_project, project: project, runner: runner
end
describe "POST /builds/register" do
it "should start a build" do
- commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit = FactoryGirl.create(:ci_commit, project: project)
commit.create_builds('master', false, nil)
build = commit.builds.first
@@ -40,8 +38,8 @@ describe Ci::API::API do
end
it "should return 404 error if no builds for specific runner" do
- commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+ commit = FactoryGirl.create(:ci_commit, project: shared_project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
post ci_api("/builds/register"), token: runner.token
@@ -49,8 +47,8 @@ describe Ci::API::API do
end
it "should return 404 error if no builds for shared runner" do
- commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
- FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+ commit = FactoryGirl.create(:ci_commit, project: project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending')
post ci_api("/builds/register"), token: shared_runner.token
@@ -58,7 +56,7 @@ describe Ci::API::API do
end
it "returns options" do
- commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit = FactoryGirl.create(:ci_commit, project: project)
commit.create_builds('master', false, nil)
post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
@@ -68,7 +66,7 @@ describe Ci::API::API do
end
it "returns variables" do
- commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit = FactoryGirl.create(:ci_commit, project: project)
commit.create_builds('master', false, nil)
project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
@@ -79,13 +77,13 @@ describe Ci::API::API do
{ "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
{ "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
{ "key" => "DB_NAME", "value" => "postgres", "public" => true },
- { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }
])
end
it "returns variables for triggers" do
trigger = FactoryGirl.create(:ci_trigger, project: project)
- commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit = FactoryGirl.create(:ci_commit, project: project)
trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
commit.create_builds('master', false, nil, trigger_request)
@@ -106,7 +104,7 @@ describe Ci::API::API do
end
describe "PUT /builds/:id" do
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project)}
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project)}
let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
it "should update a running build" do
@@ -122,5 +120,191 @@ describe Ci::API::API do
expect(build.reload.trace).to eq 'hello_world'
end
end
+
+ context "Artifacts" do
+ let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
+ let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
+ let(:commit) { FactoryGirl.create(:ci_commit, project: project) }
+ let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
+ let(: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") }
+ let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
+ let(:headers) { { "GitLab-Workhorse" => "1.0" } }
+ let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
+
+ describe "POST /builds/:id/artifacts/authorize" do
+ context "should authorize posting artifact to running build" do
+ before do
+ build.run!
+ end
+
+ it "using token as parameter" do
+ post authorize_url, { token: build.token }, headers
+ expect(response.status).to eq(200)
+ expect(json_response["TempPath"]).to_not 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
+ end
+ end
+
+ context "should fail to post too large artifact" do
+ before do
+ build.run!
+ end
+
+ it "using token as parameter" do
+ stub_application_setting(max_artifacts_size: 0)
+ post authorize_url, { token: build.token, filesize: 100 }, headers
+ expect(response.status).to eq(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)
+ end
+ end
+
+ context "should get denied" do
+ it do
+ post authorize_url, { token: 'invalid', filesize: 100 }
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ describe "POST /builds/:id/artifacts" do
+ context "Disable sanitizer" do
+ before do
+ # by configuring this path we allow to pass temp file from any path
+ allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/')
+ end
+
+ context "should post artifact to running build" do
+ before do
+ build.run!
+ end
+
+ 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)
+ 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)
+ 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)
+ end
+ end
+
+ context "should fail to post too large artifact" do
+ before do
+ build.run!
+ end
+
+ it do
+ stub_application_setting(max_artifacts_size: 0)
+ upload_artifacts(file_upload, headers_with_token)
+ expect(response.status).to eq(413)
+ end
+ end
+
+ context "should fail to post artifacts without file" do
+ before do
+ build.run!
+ end
+
+ it do
+ post post_url, {}, headers_with_token
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context "should fail to post artifacts without GitLab-Workhorse" do
+ before do
+ build.run!
+ end
+
+ it do
+ post post_url, { token: build.token }, {}
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context "should fail to post artifacts for outside of tmp path" do
+ before do
+ # by configuring this path we allow to pass file from @tmpdir only
+ # but all temporary files are stored in system tmp directory
+ @tmpdir = Dir.mktmpdir
+ allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir)
+ build.run!
+ end
+
+ after do
+ FileUtils.remove_entry @tmpdir
+ end
+
+ it do
+ upload_artifacts(file_upload, headers_with_token)
+ expect(response.status).to eq(400)
+ end
+ end
+
+ def upload_artifacts(file, headers = {}, accelerated = true)
+ if accelerated
+ post post_url, {
+ 'file.path' => file.path,
+ 'file.name' => file.original_filename
+ }, headers
+ else
+ post post_url, { file: file }, headers
+ end
+ end
+ end
+
+ describe "DELETE /builds/:id/artifacts" do
+ before do
+ build.run!
+ post delete_url, token: build.token, file: file_upload
+ end
+
+ it "should delete artifact build" do
+ build.success
+ delete delete_url, token: build.token
+ expect(response.status).to eq(200)
+ end
+ end
+
+ describe "GET /builds/:id/artifacts" do
+ before do
+ build.run!
+ end
+
+ it "should download artifact" do
+ build.update_attributes(artifacts_file: file_upload)
+ get get_url, token: build.token
+ expect(response.status).to eq(200)
+ end
+
+ it "should fail to download if no artifact uploaded" do
+ get get_url, token: build.token
+ expect(response.status).to eq(404)
+ end
+ end
+ end
end
end
diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb
deleted file mode 100644
index 6049135fd10..00000000000
--- a/spec/requests/ci/api/commits_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-require 'spec_helper'
-
-describe Ci::API::API, 'Commits' do
- include ApiHelpers
-
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
-
- let(:options) do
- {
- project_token: project.token,
- project_id: project.id
- }
- end
-
- describe "GET /commits" do
- before { commit }
-
- it "should return commits per project" do
- get ci_api("/commits"), options
-
- expect(response.status).to eq(200)
- expect(json_response.count).to eq(1)
- expect(json_response.first["project_id"]).to eq(project.id)
- expect(json_response.first["sha"]).to eq(commit.sha)
- end
- end
-
- describe "POST /commits" do
- let(:data) do
- {
- "before" => "95790bf891e76fee5e1747ab589903a6a1f80f22",
- "after" => "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
- "ref" => "refs/heads/master",
- "commits" => [
- {
- "id" => "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "message" => "Update Catalan translation to e38cb41.",
- "timestamp" => "2011-12-12T14:27:31+02:00",
- "url" => "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
- "author" => {
- "name" => "Jordi Mallach",
- "email" => "jordi@softcatala.org",
- }
- }
- ]
- }
- end
-
- it "should create a build" do
- post ci_api("/commits"), options.merge(data: data)
-
- expect(response.status).to eq(201)
- expect(json_response['sha']).to eq("da1560886d4f094c3e6c9ef40349f7d38b5d27d7")
- end
-
- it "should return 400 error if no data passed" do
- post ci_api("/commits"), options
-
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq("400 (Bad request) \"data\" not given")
- end
- end
-end
diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb
deleted file mode 100644
index 53f7f91cc1f..00000000000
--- a/spec/requests/ci/api/projects_spec.rb
+++ /dev/null
@@ -1,266 +0,0 @@
-require 'spec_helper'
-
-describe Ci::API::API do
- include ApiHelpers
-
- let(:gitlab_url) { GitlabCi.config.gitlab_ci.url }
- let(:user) { create(:user) }
- let(:private_token) { user.private_token }
-
- let(:options) do
- {
- private_token: private_token,
- url: gitlab_url
- }
- end
-
- before do
- stub_gitlab_calls
- end
-
- context "requests for scoped projects" do
- # NOTE: These ids are tied to the actual projects on demo.gitlab.com
- describe "GET /projects" do
- let!(:project1) { FactoryGirl.create(:ci_project) }
- let!(:project2) { FactoryGirl.create(:ci_project) }
-
- before do
- project1.gl_project.team << [user, :developer]
- project2.gl_project.team << [user, :developer]
- end
-
- it "should return all projects on the CI instance" do
- get ci_api("/projects"), options
- expect(response.status).to eq(200)
- expect(json_response.count).to eq(2)
- expect(json_response.first["id"]).to eq(project1.id)
- expect(json_response.last["id"]).to eq(project2.id)
- end
- end
-
- describe "GET /projects/owned" do
- let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
- let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
- let!(:project1) { FactoryGirl.create(:ci_project, gl_project: gl_project1) }
- let!(:project2) { FactoryGirl.create(:ci_project, gl_project: gl_project2) }
-
- before do
- project1.gl_project.team << [user, :developer]
- project2.gl_project.team << [user, :developer]
- end
-
- it "should return all projects on the CI instance" do
- get ci_api("/projects/owned"), options
-
- expect(response.status).to eq(200)
- expect(json_response.count).to eq(2)
- end
- end
- end
-
- describe "POST /projects/:project_id/webhooks" do
- let!(:project) { FactoryGirl.create(:ci_project) }
-
- context "Valid Webhook URL" do
- let!(:webhook) { { web_hook: "http://example.com/sth/1/ala_ma_kota" } }
-
- before do
- options.merge!(webhook)
- end
-
- it "should create webhook for specified project" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/webhooks"), options
- expect(response.status).to eq(201)
- expect(json_response["url"]).to eq(webhook[:web_hook])
- end
-
- it "fails to create webhook for non existsing project" do
- post ci_api("/projects/non-existant-id/webhooks"), options
- expect(response.status).to eq(404)
- end
-
- it "non-manager is not authorized" do
- post ci_api("/projects/#{project.id}/webhooks"), options
- expect(response.status).to eq(401)
- end
- end
-
- context "Invalid Webhook URL" do
- let!(:webhook) { { web_hook: "ala_ma_kota" } }
-
- before do
- options.merge!(webhook)
- end
-
- it "fails to create webhook for not valid url" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/webhooks"), options
- expect(response.status).to eq(400)
- end
- end
-
- context "Missed web_hook parameter" do
- it "fails to create webhook for not provided url" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/webhooks"), options
- expect(response.status).to eq(400)
- end
- end
- end
-
- describe "GET /projects/:id" do
- let!(:project) { FactoryGirl.create(:ci_project) }
-
- before do
- project.gl_project.team << [user, :developer]
- end
-
- context "with an existing project" do
- it "should retrieve the project info" do
- get ci_api("/projects/#{project.id}"), options
- expect(response.status).to eq(200)
- expect(json_response['id']).to eq(project.id)
- end
- end
-
- context "with a non-existing project" do
- it "should return 404 error if project not found" do
- get ci_api("/projects/non_existent_id"), options
- expect(response.status).to eq(404)
- end
- end
- end
-
- describe "PUT /projects/:id" do
- let!(:project) { FactoryGirl.create(:ci_project) }
- let!(:project_info) { { default_ref: "develop" } }
-
- before do
- options.merge!(project_info)
- end
-
- it "should update a specific project's information" do
- project.gl_project.team << [user, :master]
- put ci_api("/projects/#{project.id}"), options
- expect(response.status).to eq(200)
- expect(json_response["default_ref"]).to eq(project_info[:default_ref])
- end
-
- it "fails to update a non-existing project" do
- put ci_api("/projects/non-existant-id"), options
- expect(response.status).to eq(404)
- end
-
- it "non-manager is not authorized" do
- put ci_api("/projects/#{project.id}"), options
- expect(response.status).to eq(401)
- end
- end
-
- describe "DELETE /projects/:id" do
- let!(:project) { FactoryGirl.create(:ci_project) }
-
- it "should delete a specific project" do
- project.gl_project.team << [user, :master]
- delete ci_api("/projects/#{project.id}"), options
- expect(response.status).to eq(200)
- expect { project.reload }.
- to raise_error(ActiveRecord::RecordNotFound)
- end
-
- it "non-manager is not authorized" do
- delete ci_api("/projects/#{project.id}"), options
- expect(response.status).to eq(401)
- end
-
- it "is getting not found error" do
- delete ci_api("/projects/not-existing_id"), options
- expect(response.status).to eq(404)
- end
- end
-
- describe "POST /projects" do
- let(:gl_project) { FactoryGirl.create :empty_project }
- let(:project_info) do
- {
- gitlab_id: gl_project.id
- }
- end
-
- let(:invalid_project_info) { {} }
-
- context "with valid project info" do
- before do
- options.merge!(project_info)
- end
-
- it "should create a project with valid data" do
- post ci_api("/projects"), options
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq(gl_project.name_with_namespace)
- end
- end
-
- context "with invalid project info" do
- before do
- options.merge!(invalid_project_info)
- end
-
- it "should error with invalid data" do
- post ci_api("/projects"), options
- expect(response.status).to eq(400)
- end
- end
-
- describe "POST /projects/:id/runners/:id" do
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:runner) { FactoryGirl.create(:ci_runner) }
-
- it "should add the project to the runner" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(201)
-
- project.reload
- expect(project.runners.first.id).to eq(runner.id)
- end
-
- it "should fail if it tries to link a non-existing project or runner" do
- post ci_api("/projects/#{project.id}/runners/non-existing"), options
- expect(response.status).to eq(404)
-
- post ci_api("/projects/non-existing/runners/#{runner.id}"), options
- expect(response.status).to eq(404)
- end
-
- it "non-manager is not authorized" do
- allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false)
- post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(401)
- end
- end
-
- describe "DELETE /projects/:id/runners/:id" do
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:runner) { FactoryGirl.create(:ci_runner) }
-
- it "should remove the project from the runner" do
- project.gl_project.team << [user, :master]
- post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
-
- expect(project.runners).to be_present
- delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(200)
-
- project.reload
- expect(project.runners).to be_empty
- end
-
- it "non-manager is not authorized" do
- delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
- expect(response.status).to eq(401)
- end
- end
- end
-end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
index 11dc089e1f5..5942aa7a1b5 100644
--- a/spec/requests/ci/api/runners_spec.rb
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -4,57 +4,37 @@ describe Ci::API::API do
include ApiHelpers
include StubGitlabCalls
+ let(:registration_token) { 'abcdefg123456' }
+
before do
stub_gitlab_calls
- end
-
- describe "GET /runners" do
- let(:gitlab_url) { GitlabCi.config.gitlab_ci.url }
- let(:private_token) { create(:user).private_token }
- let(:options) do
- {
- private_token: private_token,
- url: gitlab_url
- }
- end
-
- before do
- 5.times { FactoryGirl.create(:ci_runner) }
- end
-
- it "should retrieve a list of all runners" do
- get ci_api("/runners", nil), options
- expect(response.status).to eq(200)
- expect(json_response.count).to eq(5)
- expect(json_response.last).to have_key("id")
- expect(json_response.last).to have_key("token")
- end
+ stub_application_setting(runners_registration_token: registration_token)
end
describe "POST /runners/register" do
describe "should create a runner if token provided" do
- before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN }
+ before { post ci_api("/runners/register"), token: registration_token }
it { expect(response.status).to eq(201) }
end
describe "should create a runner with description" do
- before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, description: "server.hostname" }
+ before { post ci_api("/runners/register"), token: registration_token, description: "server.hostname" }
it { expect(response.status).to eq(201) }
it { expect(Ci::Runner.first.description).to eq("server.hostname") }
end
describe "should create a runner with tags" do
- before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, tag_list: "tag1, tag2" }
+ before { post ci_api("/runners/register"), token: registration_token, tag_list: "tag1, tag2" }
it { expect(response.status).to eq(201) }
it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) }
end
describe "should create a runner if project token provided" do
- let(:project) { FactoryGirl.create(:ci_project) }
- before { post ci_api("/runners/register"), token: project.token }
+ 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) }
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 93617fc4b3f..0ef03f9371b 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -5,9 +5,8 @@ describe Ci::API::API do
describe 'POST /projects/:project_id/refs/:ref/trigger' do
let!(:trigger_token) { 'secure token' }
- let!(:gl_project) { FactoryGirl.create(:project) }
- let!(:project) { FactoryGirl.create(:ci_project, gl_project: gl_project) }
- let!(:project2) { FactoryGirl.create(:ci_project) }
+ let!(:project) { FactoryGirl.create(:project, ci_id: 10) }
+ let!(:project2) { FactoryGirl.create(:empty_project, ci_id: 11) }
let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
let(:options) do
{
@@ -21,7 +20,7 @@ describe Ci::API::API do
context 'Handles errors' do
it 'should return bad request if token is missing' do
- post ci_api("/projects/#{project.id}/refs/master/trigger")
+ post ci_api("/projects/#{project.ci_id}/refs/master/trigger")
expect(response.status).to eq(400)
end
@@ -31,23 +30,23 @@ describe Ci::API::API do
end
it 'should return unauthorized if token is for different project' do
- post ci_api("/projects/#{project2.id}/refs/master/trigger"), options
+ post ci_api("/projects/#{project2.ci_id}/refs/master/trigger"), options
expect(response.status).to eq(401)
end
end
context 'Have a commit' do
- let(:commit) { project.commits.last }
+ let(:commit) { project.ci_commits.last }
it 'should create builds' do
- post ci_api("/projects/#{project.id}/refs/master/trigger"), options
+ 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)
end
it 'should return bad request with no builds created if there\'s no commit for that ref' do
- post ci_api("/projects/#{project.id}/refs/other-branch/trigger"), options
+ post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
expect(response.status).to eq(400)
expect(json_response['message']).to eq('No builds created')
end
@@ -58,19 +57,19 @@ describe Ci::API::API do
end
it 'should validate variables to be a hash' do
- post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: 'value')
+ post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: 'value')
expect(response.status).to eq(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.id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
+ post ci_api("/projects/#{project.ci_id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
expect(response.status).to eq(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.id}/refs/master/trigger"), options.merge(variables: variables)
+ 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)
diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb
index f7a36cd9670..bd871605c66 100644
--- a/spec/services/archive_repository_service_spec.rb
+++ b/spec/services/archive_repository_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe ArchiveRepositoryService do
+describe ArchiveRepositoryService, services: true do
let(:project) { create(:project) }
subject { ArchiveRepositoryService.new(project, "master", "zip") }
diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb
deleted file mode 100644
index e3a8fe9681b..00000000000
--- a/spec/services/ci/create_commit_service_spec.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-require 'spec_helper'
-
-module Ci
- describe CreateCommitService do
- let(:service) { CreateCommitService.new }
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:user) { nil }
-
- before do
- stub_ci_commit_to_return_yaml_file
- end
-
- describe :execute do
- context 'valid params' do
- let(:commit) do
- service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: [ { message: "Message" } ]
- )
- end
-
- it { expect(commit).to be_kind_of(Commit) }
- it { expect(commit).to be_valid }
- it { expect(commit).to be_persisted }
- it { expect(commit).to eq(project.commits.last) }
- it { expect(commit.builds.first).to be_kind_of(Build) }
- end
-
- context "skip tag if there is no build for it" do
- it "creates commit if there is appropriate job" do
- result = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: [ { message: "Message" } ]
- )
- expect(result).to be_persisted
- 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)
-
- result = service.execute(project, user,
- ref: 'refs/heads/0_1',
- before: '00000000',
- after: '31das312',
- commits: [ { message: "Message" } ]
- )
- expect(result).to be_persisted
- end
- end
-
- it 'fails commits without .gitlab-ci.yml' do
- stub_ci_commit_yaml_file(nil)
- result = service.execute(project, user,
- ref: 'refs/heads/0_1',
- before: '00000000',
- after: '31das312',
- commits: [ { message: 'Message' } ]
- )
- expect(result).to be_persisted
- expect(result.builds.any?).to be_falsey
- expect(result.status).to eq('failed')
- end
-
- describe :ci_skip? do
- let(:message) { "some message[ci skip]" }
-
- before do
- allow_any_instance_of(Ci::Commit).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.builds.any?).to be false
- expect(commit.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" }
-
- commits = [{ message: "some message" }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(commit.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')
- commits = [{ message: message }]
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit.builds.any?).to be false
- expect(commit.status).to eq("skipped")
- 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 }
-
- commits = [{ message: "message" }]
- commit = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit.builds.count(:all)).to eq(2)
-
- commit = service.execute(project, user,
- ref: 'refs/heads/master',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
- expect(commit.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')
-
- commits = [{ message: "some message" }]
-
- commit = service.execute(project, user,
- ref: 'refs/tags/0_1',
- before: '00000000',
- after: '31das312',
- commits: commits
- )
-
- expect(commit.status).to eq("failed")
- expect(commit.builds.any?).to be false
- end
- end
- end
-end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index fcafae38644..dbdc5370bd8 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -1,9 +1,8 @@
require 'spec_helper'
-describe Ci::CreateTriggerRequestService do
+describe Ci::CreateTriggerRequestService, services: true do
let(:service) { Ci::CreateTriggerRequestService.new }
- let(:gl_project) { create(:project) }
- let(:project) { create(:ci_project, gl_project: gl_project) }
+ let(:project) { create(:project) }
let(:trigger) { create(:ci_trigger, project: project) }
before do
@@ -29,7 +28,7 @@ describe Ci::CreateTriggerRequestService do
before do
stub_ci_commit_yaml_file('{}')
- FactoryGirl.create :ci_commit, gl_project: gl_project
+ FactoryGirl.create :ci_commit, project: project
end
it { expect(subject).to be_nil }
diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb
deleted file mode 100644
index 1264e17ff5e..00000000000
--- a/spec/services/ci/event_service_spec.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'spec_helper'
-
-describe Ci::EventService do
- let(:project) { FactoryGirl.create :ci_project }
- let(:user) { double(username: "root", id: 1) }
-
- before do
- Event.destroy_all
- end
-
- describe :remove_project do
- it "creates event" do
- Ci::EventService.new.remove_project(user, project)
-
- expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been removed by root")
- end
- end
-
- describe :create_project do
- it "creates event" do
- Ci::EventService.new.create_project(user, project)
-
- expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been created by root")
- end
- end
-
- describe :change_project_settings do
- it "creates event" do
- Ci::EventService.new.change_project_settings(user, project)
-
- expect(Ci::Event.last.description).to eq("User \"root\" updated projects settings")
- end
- end
-end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
index d7242d684c6..870861ad20a 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -1,17 +1,18 @@
require 'spec_helper'
module Ci
- describe ImageForBuildService do
+ describe ImageForBuildService, services: true do
let(:service) { ImageForBuildService.new }
- let(:project) { FactoryGirl.create(:ci_project) }
- let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
- let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project, ref: 'master') }
+ let(:project) { FactoryGirl.create(:empty_project) }
+ let(:commit_sha) { '01234567890123456789' }
+ let(:commit) { project.ensure_ci_commit(commit_sha) }
let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
describe :execute do
before { build }
context 'branch name' do
+ before { allow(project).to receive(:commit).and_return(OpenStruct.new(sha: commit_sha)) }
before { build.run! }
let(:image) { service.execute(project, ref: 'master') }
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index 781764627ac..e81f9e757ac 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -1,16 +1,16 @@
require 'spec_helper'
module Ci
- describe RegisterBuildService do
+ describe RegisterBuildService, services: true do
let!(:service) { RegisterBuildService.new }
- let!(:gl_project) { FactoryGirl.create :empty_project }
- let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ 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!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
before do
- specific_runner.assign_to(gl_project.ensure_gitlab_ci_project)
+ specific_runner.assign_to(project)
end
describe :execute do
@@ -47,7 +47,7 @@ module Ci
context 'allow shared runners' do
before do
- gl_project.gitlab_ci_project.update(shared_runners_enabled: true)
+ project.update(shared_runners_enabled: true)
end
context 'shared runner' do
@@ -70,6 +70,10 @@ module Ci
end
context 'disallow shared runners' do
+ before do
+ project.update(shared_runners_enabled: false)
+ end
+
context 'shared runner' do
let(:build) { service.execute(shared_runner) }
diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb
deleted file mode 100644
index aa48fcbcbfd..00000000000
--- a/spec/services/ci/web_hook_service_spec.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'spec_helper'
-
-describe Ci::WebHookService do
- let(:project) { FactoryGirl.create :ci_project }
- let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
- let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
- let(:hook) { FactoryGirl.create :ci_web_hook, project: project }
-
- describe :execute do
- it "should execute successfully" do
- stub_request(:post, hook.url).to_return(status: 200)
- expect(Ci::WebHookService.new.build_end(build)).to be_truthy
- end
- end
-
- context 'build_data' do
- it "contains all needed fields" do
- expect(build_data(build)).to include(
- :build_id,
- :project_id,
- :ref,
- :build_status,
- :build_started_at,
- :build_finished_at,
- :before_sha,
- :project_name,
- :gitlab_url,
- :build_name
- )
- end
- end
-
- def build_data(build)
- Ci::WebHookService.new.send :build_data, build
- end
-end
diff --git a/spec/services/create_commit_builds_service_spec.rb b/spec/services/create_commit_builds_service_spec.rb
new file mode 100644
index 00000000000..ea5dcfa068a
--- /dev/null
+++ b/spec/services/create_commit_builds_service_spec.rb
@@ -0,0 +1,175 @@
+require 'spec_helper'
+
+describe CreateCommitBuildsService, services: true do
+ let(:service) { CreateCommitBuildsService.new }
+ let(:project) { FactoryGirl.create(:empty_project) }
+ let(:user) { nil }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ describe :execute do
+ context 'valid params' do
+ let(:commit) do
+ service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: "Message" }]
+ )
+ 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) }
+ end
+
+ context "skip tag if there is no build for it" do
+ it "creates commit if there is appropriate job" do
+ result = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: "Message" }]
+ )
+ expect(result).to be_persisted
+ 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)
+
+ result = service.execute(project, user,
+ ref: 'refs/heads/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: "Message" }]
+ )
+ expect(result).to be_persisted
+ end
+ end
+
+ it 'skips creating ci_commit for refs without .gitlab-ci.yml' do
+ stub_ci_commit_yaml_file(nil)
+ result = service.execute(project, user,
+ ref: 'refs/heads/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: [{ message: 'Message' }]
+ )
+ expect(result).to be_falsey
+ expect(Ci::Commit.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')
+ 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
+ end
+
+ describe :ci_skip? do
+ let(:message) { "some message[ci skip]" }
+
+ before do
+ allow_any_instance_of(Ci::Commit).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")
+ 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" }
+
+ commits = [{ message: "some 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.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')
+ 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
+ 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 }
+
+ 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)
+
+ 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)
+ end
+
+ it "creates commit with failed status if yaml is invalid" do
+ stub_ci_commit_yaml_file('invalid: file')
+
+ commits = [{ message: "some message" }]
+
+ commit = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(commit).to be_persisted
+ expect(commit.status).to eq("failed")
+ expect(commit.builds.any?).to be false
+ end
+ end
+end
diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb
new file mode 100644
index 00000000000..61e5ae72f51
--- /dev/null
+++ b/spec/services/create_release_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe CreateReleaseService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+ let(:service) { CreateReleaseService.new(project, user) }
+
+ it 'creates a new release' do
+ result = service.execute(tag_name, description)
+ expect(result[:status]).to eq(:success)
+ release = project.releases.find_by(tag: tag_name)
+ expect(release).not_to be_nil
+ expect(release.description).to eq(description)
+ end
+
+ it 'raises an error if the tag does not exist' do
+ result = service.execute("foobar", description)
+ expect(result[:status]).to eq(:error)
+ end
+
+ context 'there already exists a release on a tag' do
+ before do
+ service.execute(tag_name, description)
+ end
+
+ it 'raises an error and does not update the release' do
+ result = service.execute(tag_name, 'The best release!')
+ expect(result[:status]).to eq(:error)
+ expect(project.releases.find_by(tag: tag_name).description).to eq(description)
+ end
+ end
+end
diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
index 8edabe9450b..c800dea04fa 100644
--- a/spec/services/create_snippet_service_spec.rb
+++ b/spec/services/create_snippet_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe CreateSnippetService do
+describe CreateSnippetService, services: true do
before do
@user = create :user
@admin = create :user, admin: true
diff --git a/spec/services/destroy_group_service_spec.rb b/spec/services/destroy_group_service_spec.rb
index e28564b3866..afa89b84175 100644
--- a/spec/services/destroy_group_service_spec.rb
+++ b/spec/services/destroy_group_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe DestroyGroupService do
+describe DestroyGroupService, services: true do
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:project) { create(:project, namespace: group) }
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 7756b973ecd..f6dc9d4008f 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe EventCreateService do
+describe EventCreateService, services: true do
let(:service) { EventCreateService.new }
describe 'Issues' do
diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb
new file mode 100644
index 00000000000..2bb9c3b3db3
--- /dev/null
+++ b/spec/services/git_hooks_service_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe GitHooksService, services: true do
+ include RepoHelpers
+
+ let(:user) { create :user }
+ let(:project) { create :project }
+ let(:service) { GitHooksService.new }
+
+ before do
+ @blankrev = Gitlab::Git::BLANK_SHA
+ @oldrev = sample_commit.parent_id
+ @newrev = sample_commit.id
+ @ref = 'refs/heads/feature'
+ @repo_path = project.repository.path_to_repo
+ end
+
+ describe '#execute' do
+
+ context 'when receive hooks were successful' do
+ it 'should call post-receive hook' do
+ hook = double(trigger: true)
+ 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)
+ 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).not_to receive(:run_hook).with('post-receive')
+
+ expect do
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref)
+ end.to raise_error(GitHooksService::PreReceiveError)
+ end
+ end
+
+ 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).not_to receive(:run_hook).with('post-receive')
+
+ expect do
+ service.execute(user, @repo_path, @blankrev, @newrev, @ref)
+ 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 17015d29e51..c1080ef190a 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe GitPushService do
+describe GitPushService, services: true do
include RepoHelpers
let(:user) { create :user }
@@ -265,6 +265,75 @@ describe GitPushService do
expect(Issue.find(issue.id)).to be_opened
end
end
+
+ # EE-only tests
+ context "for jira issue tracker" do
+ include JiraServiceHelper
+
+ let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
+
+ before do
+ jira_service_settings
+
+ WebMock.stub_request(:post, jira_api_transition_url)
+ WebMock.stub_request(:post, jira_api_comment_url)
+ WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
+ WebMock.stub_request(:get, jira_api_test_url)
+
+ allow(closing_commit).to receive_messages({
+ issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern),
+ safe_message: message,
+ author_name: commit_author.name,
+ author_email: commit_author.email
+ })
+
+ allow(project.repository).to receive_messages(commits_between: [closing_commit])
+ end
+
+ after do
+ jira_tracker.destroy!
+ end
+
+ context "mentioning an issue" do
+ let(:message) { "this is some work.\n\nrelated to JIRA-1" }
+
+ it "should initiate one api call to jira server to mention the issue" do
+ service.execute(project, user, @oldrev, @newrev, @ref)
+
+ expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+ body: /mentioned this issue in/
+ ).once
+ end
+ end
+
+ context "closing an issue" do
+ let(:message) { "this is some work.\n\ncloses JIRA-1" }
+
+ it "should initiate one api call to jira server to close the issue" do
+ transition_body = {
+ transition: {
+ id: '2'
+ }
+ }.to_json
+
+ service.execute(project, user, @oldrev, @newrev, @ref)
+ expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
+ body: transition_body
+ ).once
+ end
+
+ it "should initiate one api call to jira server to comment on the issue" do
+ comment_body = {
+ body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]."
+ }.to_json
+
+ service.execute(project, user, @oldrev, @newrev, @ref)
+ expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+ body: comment_body
+ ).once
+ end
+ end
+ end
end
describe "empty project" do
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index eed50c7ebac..b982274c529 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe GitTagPushService do
+describe GitTagPushService, services: true do
include RepoHelpers
let(:user) { create :user }
@@ -58,14 +58,14 @@ describe GitTagPushService do
it { is_expected.to include(timestamp: @commit.date.xmlschema) }
it do
is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- @commit.id
- ].join('/')
- )
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ @commit.id
+ ].join('/')
+ )
end
context "with a author" do
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb
index 4c62fbafd73..6a7ea4b2f44 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issues/bulk_update_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Issues::BulkUpdateService do
+describe Issues::BulkUpdateService, services: true do
let(:issue) { create(:issue, project: @project) }
before do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index db547ce0d50..3a8daf28f5e 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Issues::CloseService do
+describe Issues::CloseService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:issue) { create(:issue, assignee: user2) }
@@ -14,7 +14,9 @@ describe Issues::CloseService do
describe :execute do
context "valid params" do
before do
- @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ perform_enqueued_jobs do
+ @issue = Issues::CloseService.new(project, user, {}).execute(issue)
+ end
end
it { expect(@issue).to be_valid }
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 7f1ebcb3198..2148d091a57 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Issues::CreateService do
+describe Issues::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index a91be3b4472..87da0e9618b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1,18 +1,31 @@
require 'spec_helper'
-describe Issues::UpdateService do
+describe Issues::UpdateService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:issue) { create(:issue, title: 'Old title') }
+ let(:user3) { create(:user) }
+ let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) }
let(:label) { create(:label) }
let(:project) { issue.project }
before do
project.team << [user, :master]
project.team << [user2, :developer]
+ project.team << [user3, :developer]
end
describe 'execute' do
+ def find_note(starting_with)
+ @issue.notes.find do |note|
+ note && note.note.start_with?(starting_with)
+ 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 = {
@@ -23,7 +36,10 @@ describe Issues::UpdateService do
label_ids: [label.id]
}
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ perform_enqueued_jobs do
+ @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ end
+
@issue.reload
end
@@ -34,18 +50,14 @@ describe Issues::UpdateService do
it { expect(@issue.labels.count).to eq(1) }
it { expect(@issue.labels.first.title).to eq('Bug') }
- it 'should send email to user2 about assign of new issue' do
- email = ActionMailer::Base.deliveries.last
- expect(email.to.first).to eq(user2.email)
+ it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do
+ deliveries = ActionMailer::Base.deliveries
+ email = deliveries.last
+ recipients = deliveries.last(2).map(&:to).flatten
+ expect(recipients).to include(user2.email, user3.email)
expect(email.subject).to include(issue.title)
end
- def find_note(starting_with)
- @issue.notes.find do |note|
- note && note.note.start_with?(starting_with)
- end
- end
-
it 'should create system note about issue reassign' do
note = find_note('Reassigned to')
@@ -67,5 +79,71 @@ describe Issues::UpdateService do
expect(note.note).to eq 'Title changed from **Old title** to **New title**'
end
end
+
+ context 'when Issue has tasks' do
+ before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+
+ it { expect(@issue.tasks?).to eq(true) }
+
+ context 'when tasks are marked as completed' do
+ before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) }
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as completed')
+ note2 = find_note('Marked the task **Task 2** as completed')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+
+ context 'when tasks are marked as incomplete' do
+ before do
+ update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
+ update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as incomplete')
+ note2 = find_note('Marked the task **Task 2** as incomplete')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+
+ context 'when tasks position has been modified' do
+ before do
+ update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
+ update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" })
+ end
+
+ it 'does not create a system note' do
+ note = find_note('Marked the task **Task 2** as incomplete')
+
+ expect(note).to be_nil
+ end
+ end
+
+ context 'when a Task list with a completed item is totally replaced' do
+ before do
+ update_issue({ description: "- [ ] Task 1\n- [X] Task 2" })
+ update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ end
+
+ it 'does not create a system note referencing the position the old item' do
+ note = find_note('Marked the task **Two** as incomplete')
+
+ expect(note).to be_nil
+ end
+
+ it 'should not generate a new note at all' do
+ expect do
+ update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ end.not_to change { Note.count }
+ end
+ end
+ end
+
end
end
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index b3cbfd4b5b8..50d0c288790 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::CloseService do
+describe MergeRequests::CloseService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:merge_request) { create(:merge_request, assignee: user2) }
@@ -18,7 +18,9 @@ describe MergeRequests::CloseService do
before do
allow(service).to receive(:execute_hooks)
- @merge_request = service.execute(merge_request)
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ end
end
it { expect(@merge_request).to be_valid }
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index cc64d69361e..be8f1676eeb 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::CreateService do
+describe MergeRequests::CreateService, services: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 7483f51de03..ceb3f97280e 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::MergeService do
+describe MergeRequests::MergeService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:merge_request) { create(:merge_request, assignee: user2) }
@@ -13,12 +13,14 @@ describe MergeRequests::MergeService do
describe :execute do
context 'valid params' do
- let(:service) { MergeRequests::MergeService.new(project, user, {}) }
+ let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
before do
allow(service).to receive(:execute_hooks)
- service.execute(merge_request, 'Awesome message')
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
end
it { expect(merge_request).to be_valid }
@@ -37,14 +39,14 @@ describe MergeRequests::MergeService do
end
context "error handling" do
- let(:service) { MergeRequests::MergeService.new(project, user, {}) }
+ let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
it 'saves error if there is an exception' do
allow(service).to receive(:repository).and_raise("error")
allow(service).to receive(:execute_hooks)
- service.execute(merge_request, 'Awesome message')
+ service.execute(merge_request)
expect(merge_request.merge_error).to eq("Something went wrong during merge")
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
new file mode 100644
index 00000000000..449cecaa789
--- /dev/null
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe MergeRequests::MergeWhenBuildSucceedsService do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request) }
+
+ let(:mr_merge_if_green_enabled) do
+ create(:merge_request, merge_when_build_succeeds: true, merge_user: user,
+ source_branch: "source_branch", target_branch: project.default_branch,
+ source_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(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, commit_message: 'Awesome message') }
+
+ describe "#execute" do
+ context 'first time enabling' do
+ before do
+ allow(merge_request).to receive(:ci_commit).and_return(ci_commit)
+ service.execute(merge_request)
+ end
+
+ it 'sets the params, merge_user, and flag' do
+ expect(merge_request).to be_valid
+ expect(merge_request.merge_when_build_succeeds).to be_truthy
+ expect(merge_request.merge_params).to eq commit_message: 'Awesome message'
+ expect(merge_request.merge_user).to be user
+ end
+
+ it 'creates a system note' do
+ note = merge_request.notes.last
+ expect(note.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-z]{8}/
+ end
+ end
+
+ context 'already approved' do
+ let(:service) { MergeRequests::MergeWhenBuildSucceedsService.new(project, user, new_key: true) }
+ 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(:mergeable?).and_return(true)
+ allow(ci_commit).to receive(:success?).and_return(true)
+ end
+
+ it 'updates the merge params' do
+ expect(SystemNoteService).not_to receive(:merge_when_build_succeeds)
+
+ service.execute(mr_merge_if_green_enabled)
+ expect(mr_merge_if_green_enabled.merge_params).to have_key(:new_key)
+ end
+ end
+ end
+
+ describe "#trigger" 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)
+
+ expect(MergeWorker).to receive(:perform_async)
+ service.trigger(build)
+ end
+ end
+
+ describe "#cancel" do
+ before do
+ service.cancel(mr_merge_if_green_enabled)
+ end
+
+ it "resets all the merge_when_build_succeeds params" do
+ expect(mr_merge_if_green_enabled.merge_when_build_succeeds).to be_falsey
+ expect(mr_merge_if_green_enabled.merge_params).to eq({})
+ expect(mr_merge_if_green_enabled.merge_user).to be nil
+ end
+
+ it 'Posts a system note' do
+ note = mr_merge_if_green_enabled.notes.last
+ expect(note.note).to include 'Canceled the automatic merge'
+ end
+ end
+end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 227ac995ec2..450250ba032 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::RefreshService do
+describe MergeRequests::RefreshService, services: true do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:service) { MergeRequests::RefreshService }
@@ -17,7 +17,9 @@ describe MergeRequests::RefreshService do
source_project: @project,
source_branch: 'master',
target_branch: 'feature',
- target_project: @project)
+ target_project: @project,
+ merge_when_build_succeeds: true,
+ merge_user: @user)
@fork_merge_request = create(:merge_request,
source_project: @fork_project,
@@ -46,6 +48,7 @@ describe MergeRequests::RefreshService do
it { expect(@merge_request.notes).not_to be_empty }
it { expect(@merge_request).to be_open }
+ 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 }
end
@@ -62,6 +65,25 @@ describe MergeRequests::RefreshService do
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
end
+ context 'manual merge of source branch' do
+ before do
+ # Merge master -> feature branch
+ author = { email: 'test@gitlab.com', time: Time.now, name: "Me" }
+ commit_options = { message: 'Test message', committer: author, author: author }
+ master_commit = @project.repository.commit('master')
+ @project.repository.merge(@user, master_commit.id, 'feature', commit_options)
+ commit = @project.repository.commit('feature')
+ service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
+ reload_mrs
+ end
+
+ it { expect(@merge_request.notes.last.note).to include('changed to merged') }
+ it { expect(@merge_request).to be_merged }
+ it { expect(@merge_request.diffs.length).to be > 0 }
+ it { expect(@fork_merge_request).to be_merged }
+ it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
+ end
+
context 'push to fork repo source branch' do
let(:refresh_service) { service.new(@fork_project, @user) }
before do
@@ -127,6 +149,7 @@ describe MergeRequests::RefreshService do
end
end
+
def reload_mrs
@merge_request.reload
@fork_merge_request.reload
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 9401bc3b558..ac0221998f5 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MergeRequests::ReopenService do
+describe MergeRequests::ReopenService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:merge_request) { create(:merge_request, assignee: user2) }
@@ -19,7 +19,9 @@ describe MergeRequests::ReopenService do
allow(service).to receive(:execute_hooks)
merge_request.state = :closed
- service.execute(merge_request)
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
end
it { expect(merge_request).to be_valid }
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index c75173c1452..2e9e6e0870d 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -1,9 +1,10 @@
require 'spec_helper'
-describe MergeRequests::UpdateService do
+describe MergeRequests::UpdateService, services: true do
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let(:merge_request) { create(:merge_request, :simple, title: 'Old title') }
+ let(:user3) { create(:user) }
+ let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) }
let(:project) { merge_request.project }
let(:label) { create(:label) }
@@ -13,6 +14,17 @@ describe MergeRequests::UpdateService do
end
describe 'execute' do
+ def find_note(starting_with)
+ @merge_request.notes.find do |note|
+ note && note.note.start_with?(starting_with)
+ end
+ end
+
+ def update_merge_request(opts)
+ @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
+ @merge_request.reload
+ end
+
context 'valid params' do
let(:opts) do
{
@@ -30,8 +42,10 @@ describe MergeRequests::UpdateService do
before do
allow(service).to receive(:execute_hooks)
- @merge_request = service.execute(merge_request)
- @merge_request.reload
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ @merge_request.reload
+ end
end
it { expect(@merge_request).to be_valid }
@@ -47,18 +61,14 @@ describe MergeRequests::UpdateService do
with(@merge_request, 'update')
end
- it 'should send email to user2 about assign of new merge_request' do
- email = ActionMailer::Base.deliveries.last
- expect(email.to.first).to eq(user2.email)
+ it 'should send email to user2 about assign of new merge request and email to user3 about merge request unassignment' do
+ deliveries = ActionMailer::Base.deliveries
+ email = deliveries.last
+ recipients = deliveries.last(2).map(&:to).flatten
+ expect(recipients).to include(user2.email, user3.email)
expect(email.subject).to include(merge_request.title)
end
- def find_note(starting_with)
- @merge_request.notes.find do |note|
- note && note.note.start_with?(starting_with)
- end
- end
-
it 'should create system note about merge_request reassign' do
note = find_note('Reassigned to')
@@ -87,5 +97,39 @@ describe MergeRequests::UpdateService do
expect(note.note).to eq 'Target branch changed from `master` to `target`'
end
end
+
+ context 'when MergeRequest has tasks' do
+ before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+
+ it { expect(@merge_request.tasks?).to eq(true) }
+
+ context 'when tasks are marked as completed' do
+ before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) }
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as completed')
+ note2 = find_note('Marked the task **Task 2** as completed')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+
+ context 'when tasks are marked as incomplete' do
+ before do
+ update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" })
+ update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" })
+ end
+
+ it 'creates system note about task status change' do
+ note1 = find_note('Marked the task **Task 1** as incomplete')
+ note2 = find_note('Marked the task **Task 2** as incomplete')
+
+ expect(note1).not_to be_nil
+ expect(note2).not_to be_nil
+ end
+ end
+ end
+
end
end
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
new file mode 100644
index 00000000000..1cd6eb2ab38
--- /dev/null
+++ b/spec/services/milestones/close_service_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Milestones::CloseService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe :execute do
+ before do
+ Milestones::CloseService.new(project, user, {}).execute(milestone)
+ end
+
+ it { expect(milestone).to be_valid }
+ it { expect(milestone).to be_closed }
+
+ describe :event do
+ let(:event) { Event.first }
+
+ it { expect(event.milestone).to be_truthy }
+ it { expect(event.target).to eq(milestone) }
+ it { expect(event.action_name).to eq('closed') }
+ end
+ end
+end
diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb
new file mode 100644
index 00000000000..c793026e300
--- /dev/null
+++ b/spec/services/milestones/create_service_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Milestones::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ describe :execute do
+ context "valid params" do
+ before do
+ project.team << [user, :master]
+
+ opts = {
+ title: 'v2.1.9',
+ description: 'Patch release to fix security issue'
+ }
+
+ @milestone = Milestones::CreateService.new(project, user, opts).execute
+ end
+
+ it { expect(@milestone).to be_valid }
+ it { expect(@milestone.title).to eq('v2.1.9') }
+ end
+ end
+end
diff --git a/spec/services/milestones/group_service_spec.rb b/spec/services/milestones/group_service_spec.rb
deleted file mode 100644
index 74eb0f99e0f..00000000000
--- a/spec/services/milestones/group_service_spec.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-require 'spec_helper'
-
-describe Milestones::GroupService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
- let(:group) { create(:group) }
- let(:project1) { create(:project, group: group) }
- let(:project2) { create(:project, path: 'gitlab-ci', group: group) }
- let(:project3) { create(:project, path: 'cookbook-gitlab', group: group) }
- let(:milestone1_project1) { create(:milestone, title: "Milestone v1.2", project: project1) }
- let(:milestone1_project2) { create(:milestone, title: "Milestone v1.2", project: project2) }
- let(:milestone1_project3) { create(:milestone, title: "Milestone v1.2", project: project3) }
- let(:milestone2_project1) { create(:milestone, title: "VD-123", project: project1) }
- let(:milestone2_project2) { create(:milestone, title: "VD-123", project: project2) }
- let(:milestone2_project3) { create(:milestone, title: "VD-123", project: project3) }
-
- describe 'execute' do
- context 'with valid projects' do
- before do
- milestones =
- [
- milestone1_project1,
- milestone1_project2,
- milestone1_project3,
- milestone2_project1,
- milestone2_project2,
- milestone2_project3
- ]
- @group_milestones = Milestones::GroupService.new(milestones).execute
- end
-
- it 'should have all project milestones' do
- expect(@group_milestones.count).to eq(2)
- end
-
- it 'should have all project milestones titles' do
- expect(@group_milestones.map { |group_milestone| group_milestone.title }).to match_array(['Milestone v1.2', 'VD-123'])
- end
-
- it 'should have all project milestones' do
- expect(@group_milestones.map { |group_milestone| group_milestone.milestones.count }.sum).to eq(6)
- end
- end
- end
-
- describe 'milestone' do
- context 'with valid title' do
- before do
- milestones =
- [
- milestone1_project1,
- milestone1_project2,
- milestone1_project3,
- milestone2_project1,
- milestone2_project2,
- milestone2_project3
- ]
- @group_milestones = Milestones::GroupService.new(milestones).milestone('Milestone v1.2')
- end
-
- it 'should have exactly one group milestone' do
- expect(@group_milestones.title).to eq('Milestone v1.2')
- end
-
- it 'should have all project milestones with the same title' do
- expect(@group_milestones.milestones.count).to eq(3)
- end
- end
- end
-end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index f2ea0805b2f..a797a2fe4aa 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Notes::CreateService do
+describe Notes::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:issue) { create(:issue, project: project) }
let(:user) { create(:user) }
@@ -24,4 +24,38 @@ describe Notes::CreateService do
it { expect(@note.note).to eq('Awesome comment') }
end
end
+
+ describe "award emoji" do
+ before do
+ project.team << [user, :master]
+ end
+
+ it "creates emoji note" do
+ opts = {
+ note: ':smile: ',
+ 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('smile')
+ expect(@note.is_award).to be_truthy
+ end
+
+ it "creates regular note if emoji name is invalid" do
+ opts = {
+ note: ':smile: moretext: ',
+ 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])
+ expect(@note.is_award).to be_falsy
+ end
+ end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 520140917aa..6d219f35895 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1,8 +1,14 @@
require 'spec_helper'
-describe NotificationService do
+describe NotificationService, services: true do
let(:notification) { NotificationService.new }
+ around(:each) do |example|
+ perform_enqueued_jobs do
+ example.run
+ end
+ end
+
describe 'Keys' do
describe :new_key do
let!(:key) { create(:personal_key) }
@@ -10,8 +16,7 @@ describe NotificationService do
it { expect(notification.new_key(key)).to be_truthy }
it 'should sent email to key owner' do
- expect(Notify).to receive(:new_ssh_key_email).with(key.id)
- notification.new_key(key)
+ expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
end
@@ -23,8 +28,7 @@ describe NotificationService do
it { expect(notification.new_email(email)).to be_truthy }
it 'should send email to email owner' do
- expect(Notify).to receive(:new_email_email).with(email.id)
- notification.new_email(email)
+ expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1)
end
end
end
@@ -41,24 +45,32 @@ describe NotificationService do
project.team << [issue.author, :master]
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')
end
describe :new_note do
it do
add_users_with_subscription(note.project, issue)
- should_email(@u_watcher.id)
- should_email(note.noteable.author_id)
- should_email(note.noteable.assignee_id)
- should_email(@u_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(note.author_id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_outsider_mentioned)
+ # Ensure create SentNotification by noteable = issue 6 times, not noteable = note
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times
+
+ ActionMailer::Base.deliveries.clear
notification.new_note(note)
+
+ should_email(@u_watcher)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignee)
+ should_email(@u_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_email(@subscribed_participant)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_outsider_mentioned)
end
it 'filters out "mentioned in" notes' do
@@ -82,26 +94,20 @@ describe NotificationService do
group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id)
group_member.notification_level = Notification::N_GLOBAL
group_member.save
+ ActionMailer::Base.deliveries.clear
end
it do
- should_email(note.noteable.author_id)
- should_email(note.noteable.assignee_id)
- should_email(@u_mentioned.id)
- should_not_email(@u_watcher.id)
- should_not_email(note.author_id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.new_note(note)
- end
- end
- def should_email(user_id)
- expect(Notify).to receive(:note_issue_email).with(user_id, note.id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignee)
+ should_email(@u_mentioned)
+ should_not_email(@u_watcher)
+ should_not_email(note.author)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
+ end
end
end
@@ -113,24 +119,29 @@ describe NotificationService do
before do
build_team(note.project)
+ note.project.team << [note.author, :master]
+ ActionMailer::Base.deliveries.clear
end
describe :new_note do
it do
+ notification.new_note(note)
+
# Notify all team members
note.project.team.members.each do |member|
# User with disabled notification should not be notified
next if member.id == @u_disabled.id
- should_email(member.id)
+ # Author should not be notified
+ next if member.id == note.author.id
+ should_email(member)
end
- should_email(note.noteable.author_id)
- should_email(note.noteable.assignee_id)
- should_not_email(note.author_id)
- should_not_email(@u_mentioned.id)
- should_not_email(@u_disabled.id)
- should_not_email(@u_not_mentioned.id)
- notification.new_note(note)
+ should_email(note.noteable.author)
+ should_email(note.noteable.assignee)
+ should_not_email(note.author)
+ should_email(@u_mentioned)
+ should_not_email(@u_disabled)
+ should_email(@u_not_mentioned)
end
it 'filters out "mentioned in" notes' do
@@ -140,14 +151,6 @@ describe NotificationService do
notification.new_note(mentioned_note)
end
end
-
- def should_email(user_id)
- expect(Notify).to receive(:note_issue_email).with(user_id, note.id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:note_issue_email).with(user_id, note.id)
- end
end
context 'commit note' do
@@ -156,43 +159,38 @@ describe NotificationService do
before do
build_team(note.project)
+ ActionMailer::Base.deliveries.clear
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
end
- describe :new_note do
+ describe :new_note, :perform_enqueued_jobs do
it do
- should_email(@u_committer.id, note)
- should_email(@u_watcher.id, note)
- should_not_email(@u_mentioned.id, note)
- should_not_email(note.author_id, note)
- should_not_email(@u_participating.id, note)
- should_not_email(@u_disabled.id, note)
notification.new_note(note)
+
+ 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)
end
it do
note.update_attribute(:note, '@mention referenced')
- should_email(@u_committer.id, note)
- should_email(@u_watcher.id, note)
- should_email(@u_mentioned.id, note)
- should_not_email(note.author_id, note)
- should_not_email(@u_participating.id, note)
- should_not_email(@u_disabled.id, note)
notification.new_note(note)
+
+ 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)
end
it do
@u_committer.update_attributes(notification_level: Notification::N_MENTION)
- should_not_email(@u_committer.id, note)
notification.new_note(note)
- end
-
- def should_email(user_id, n)
- expect(Notify).to receive(:note_commit_email).with(user_id, n.id)
- end
-
- def should_not_email(user_id, n)
- expect(Notify).not_to receive(:note_commit_email).with(user_id, n.id)
+ should_not_email(@u_committer)
end
end
end
@@ -205,99 +203,71 @@ describe NotificationService do
before do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
+ ActionMailer::Base.deliveries.clear
end
describe :new_issue do
it do
- should_email(issue.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_not_email(@u_mentioned.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.new_issue(issue, @u_disabled)
+
+ should_email(issue.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_not_email(@u_mentioned)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
it do
issue.assignee.update_attributes(notification_level: Notification::N_MENTION)
- should_not_email(issue.assignee_id)
notification.new_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:new_issue_email).with(user_id, issue.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:new_issue_email).with(user_id, issue.id)
+ should_not_email(issue.assignee)
end
end
describe :reassigned_issue do
it 'should email new assignee' do
- should_email(issue.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
-
notification.reassigned_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:reassigned_issue_email).with(user_id, issue.id, nil, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:reassigned_issue_email).with(user_id, issue.id, issue.assignee_id, @u_disabled.id)
+ should_email(issue.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :close_issue do
it 'should sent email to issue assignee and issue author' do
- should_email(issue.assignee_id)
- should_email(issue.author_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
-
notification.close_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:closed_issue_email).with(user_id, issue.id, @u_disabled.id)
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :reopen_issue do
it 'should send email to issue assignee and issue author' do
- should_email(issue.assignee_id)
- should_email(issue.author_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
-
notification.reopen_issue(issue, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:issue_status_changed_email).with(user_id, issue.id, 'reopened', @u_disabled.id)
+ should_email(issue.assignee)
+ should_email(issue.author)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
end
end
end
@@ -309,108 +279,79 @@ describe NotificationService do
before do
build_team(merge_request.target_project)
add_users_with_subscription(merge_request.target_project, merge_request)
+ ActionMailer::Base.deliveries.clear
end
describe :new_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.new_merge_request(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:new_merge_request_email).with(user_id, merge_request.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:new_merge_request_email).with(user_id, merge_request.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@watcher_and_subscriber)
+ should_email(@u_participant_mentioned)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :reassigned_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.reassigned_merge_request(merge_request, merge_request.author)
- end
- def should_email(user_id)
- expect(Notify).to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, nil, merge_request.author_id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:reassigned_merge_request_email).with(user_id, merge_request.id, merge_request.assignee_id, merge_request.author_id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :closed_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.close_mr(merge_request, @u_disabled)
- end
- def should_email(user_id)
- expect(Notify).to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:closed_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :merged_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.merge_mr(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:merged_merge_request_email).with(user_id, merge_request.id, @u_disabled.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
describe :reopen_merge_request do
it do
- should_email(merge_request.assignee_id)
- should_email(@u_watcher.id)
- should_email(@u_participant_mentioned.id)
- should_email(@subscriber.id)
- should_not_email(@unsubscriber.id)
- should_not_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.reopen_mr(merge_request, @u_disabled)
- end
-
- def should_email(user_id)
- expect(Notify).to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id)
- end
- def should_not_email(user_id)
- expect(Notify).not_to receive(:merge_request_status_email).with(user_id, merge_request.id, 'reopened', @u_disabled.id)
+ should_email(merge_request.assignee)
+ should_email(@u_watcher)
+ should_email(@u_participant_mentioned)
+ should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
+ should_not_email(@unsubscriber)
+ should_not_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
end
@@ -420,22 +361,16 @@ describe NotificationService do
before do
build_team(project)
+ ActionMailer::Base.deliveries.clear
end
describe :project_was_moved do
it do
- should_email(@u_watcher.id)
- should_email(@u_participating.id)
- should_not_email(@u_disabled.id)
notification.project_was_moved(project, "gitlab/gitlab")
- end
- def should_email(user_id)
- expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab")
- end
-
- def should_not_email(user_id)
- expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab")
+ should_email(@u_watcher)
+ should_email(@u_participating)
+ should_not_email(@u_disabled)
end
end
end
@@ -462,11 +397,30 @@ describe NotificationService do
def add_users_with_subscription(project, issuable)
@subscriber = create :user
@unsubscriber = create :user
+ @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING)
+ @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH)
+ project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
project.team << [@unsubscriber, :master]
+ project.team << [@watcher_and_subscriber, :master]
issuable.subscriptions.create(user: @subscriber, subscribed: true)
+ issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
+ # Make the watcher a subscriber to detect dupes
+ issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
+ end
+
+ def sent_to_user?(user)
+ ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1
+ end
+
+ def should_email(user)
+ expect(sent_to_user?(user)).to be_truthy
+ end
+
+ def should_not_email(user)
+ expect(sent_to_user?(user)).to be_falsey
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 25277f07482..5d0b18558b1 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::CreateService do
+describe Projects::CreateService, services: true do
describe :create_by_user do
before do
@user = create :user
@@ -49,6 +49,13 @@ describe Projects::CreateService do
it { expect(@project.namespace).to eq(@group) }
end
+ context 'error handling' do
+ it 'handles invalid options' do
+ @opts.merge!({ default_branch: 'master' } )
+ expect(create_project(@user, @opts)).to eq(nil)
+ end
+ end
+
context 'wiki_enabled creates repository directory' do
context 'wiki_enabled true creates wiki repository directory' do
before do
@@ -70,6 +77,28 @@ describe Projects::CreateService do
end
end
+ context 'builds_enabled global setting' do
+ let(:project) { create_project(@user, @opts) }
+
+ subject { project.builds_enabled? }
+
+ context 'global builds_enabled false does not enable CI by default' do
+ before do
+ @opts.merge!(builds_enabled: false)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'global builds_enabled true does enable CI by default' do
+ before do
+ @opts.merge!(builds_enabled: true)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
context 'restricted visibility level' do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index e83eef0b1a2..1ec27077717 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::DestroyService do
+describe Projects::DestroyService, services: true do
let!(:user) { create(:user) }
let!(:project) { create(:project, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo }
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index ddee2e62dfc..5ceed5af9a5 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::DownloadService do
+describe Projects::DownloadService, services: true do
describe 'File service' do
before do
@user = create :user
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index 65a8c81204d..d1ee60a0aea 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::ForkService do
+describe Projects::ForkService, services: true do
describe :fork_by_user do
before do
@from_namespace = create(:namespace)
@@ -25,13 +25,6 @@ describe Projects::ForkService do
end
end
- context 'fork project failure' do
- it "fails due to transaction failure" do
- @to_project = fork_project(@from_project, @to_user, false)
- expect(@to_project.import_failed?)
- end
- end
-
context 'project already exists' do
it "should fail due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@@ -46,7 +39,7 @@ describe Projects::ForkService do
it "fork and enable CI for fork" do
@from_project.enable_ci
@to_project = fork_project(@from_project, @to_user)
- expect(@to_project.gitlab_ci?).to be_truthy
+ expect(@to_project.builds_enabled?).to be_truthy
end
end
end
@@ -66,7 +59,7 @@ describe Projects::ForkService do
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
- to_project = fork_project(@project, @group_owner, true, @opts)
+ to_project = fork_project(@project, @group_owner, @opts)
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
@@ -78,7 +71,7 @@ describe Projects::ForkService do
context 'fork project for group when user not owner' do
it 'group developer should fail to fork project into the group' do
- to_project = fork_project(@project, @developer, true, @opts)
+ to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
@@ -87,7 +80,7 @@ describe Projects::ForkService do
it 'should fail due to validation, not transaction failure' do
existing_project = create(:project, name: @project.name,
namespace: @group)
- to_project = fork_project(@project, @group_owner, true, @opts)
+ to_project = fork_project(@project, @group_owner, @opts)
expect(existing_project.persisted?).to be_truthy
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
@@ -95,8 +88,8 @@ describe Projects::ForkService do
end
end
- def fork_project(from_project, user, fork_success = true, params = {})
- allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success)
+ def fork_project(from_project, user, params = {})
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
Projects::ForkService.new(from_project, user, params).execute
end
end
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 47755bfc990..c46259431aa 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::TransferService do
+describe Projects::TransferService, services: true do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index b347fa15f87..3c06a890163 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::UpdateService do
+describe Projects::UpdateService, services: true do
describe :update_by_user do
before do
@user = create :user
@@ -100,6 +100,45 @@ describe Projects::UpdateService do
end
end
+ describe :visibility_level do
+ let(:user) { create :user, admin: true }
+ let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
+ let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL }
+ let(:opts) { {} }
+
+ before do
+ forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id)
+ forked_project.save
+
+ @created_internal = project.internal?
+ @fork_created_internal = forked_project.internal?
+ end
+
+ context 'should update forks visibility level when parent set to more restrictive' do
+ before do
+ opts.merge!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ update_project(project, user, opts).inspect
+ end
+
+ it { expect(@created_internal).to be_truthy }
+ it { expect(@fork_created_internal).to be_truthy }
+ it { expect(project.private?).to be_truthy }
+ it { expect(project.forks.first.private?).to be_truthy }
+ end
+
+ context 'should not update forks visibility level when parent set to less restrictive' do
+ before do
+ opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ update_project(project, user, opts).inspect
+ end
+
+ it { expect(@created_internal).to be_truthy }
+ it { expect(@fork_created_internal).to be_truthy }
+ it { expect(project.public?).to be_truthy }
+ it { expect(project.forks.first.internal?).to be_truthy }
+ end
+ end
+
def update_project(project, user, opts)
Projects::UpdateService.new(project, user, opts).execute
end
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index 1b1a80d1fe7..9268a9fb1a2 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Projects::UploadService do
+describe Projects::UploadService, services: true do
describe 'File service' do
before do
@user = create :user
diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb
index f57bfaea879..7b3a9a75d7c 100644
--- a/spec/services/search_service_spec.rb
+++ b/spec/services/search_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Search::GlobalService' do
+describe 'Search::GlobalService', services: true do
let(:user) { create(:user) }
let(:public_user) { create(:user) }
let(:internal_user) { create(:user) }
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index a31fc1e4b07..4455ae7b321 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SystemHooksService do
+describe SystemHooksService, services: true do
let(:user) { create :user }
let(:project) { create :project }
let(:project_member) { create :project_member }
@@ -9,37 +9,54 @@ describe SystemHooksService do
let(:group_member) { create(:group_member) }
context 'event data' do
- it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
- it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) }
+ it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id) }
+ it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
it do
+ project.old_path_with_namespace = 'renamed_from_path'
+ expect(event_data(project, :rename)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+ it do
+ project.old_path_with_namespace = 'transfered_from_path'
+ expect(event_data(project, :transfer)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+
+ it do
expect(event_data(group, :create)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group, :destroy)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group_member, :create)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_name, :user_email, :group_access
)
end
it do
expect(event_data(group_member, :destroy)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_name, :user_email, :group_access
)
end
end
@@ -49,6 +66,8 @@ describe SystemHooksService do
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
+ it { expect(event_name(project, :rename)).to eq "project_rename" }
+ it { expect(event_name(project, :transfer)).to eq "project_transfer" }
it { expect(event_name(project_member, :create)).to eq "user_add_to_team" }
it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" }
it { expect(event_name(key, :create)).to eq 'key_create' }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index a45130bd473..c9f828ae2f7 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe SystemNoteService do
+describe SystemNoteService, services: true do
let(:project) { create(:project) }
let(:author) { create(:user) }
let(:noteable) { create(:issue, project: project) }
@@ -207,6 +207,32 @@ describe SystemNoteService do
end
end
+ describe '.merge_when_build_succeeds' do
+ let(:ci_commit) { build :ci_commit_without_jobs }
+ let(:noteable) { create :merge_request }
+
+ subject { described_class.merge_when_build_succeeds(noteable, project, author, noteable.last_commit) }
+
+ it_behaves_like 'a system note'
+
+ it "posts the Merge When Build Succeeds system note" do
+ expect(subject.note).to match /Enabled an automatic merge when the build for (\w+\/\w+@)?[0-9a-f]{40} succeeds/
+ end
+ end
+
+ describe '.cancel_merge_when_build_succeeds' do
+ let(:ci_commit) { build :ci_commit_without_jobs }
+ let(:noteable) { create :merge_request }
+
+ subject { described_class.cancel_merge_when_build_succeeds(noteable, project, author) }
+
+ it_behaves_like 'a system note'
+
+ it "posts the Merge When Build Succeeds system note" do
+ expect(subject.note).to eq "Canceled the automatic merge"
+ end
+ end
+
describe '.change_title' do
subject { described_class.change_title(noteable, project, author, 'Old title') }
@@ -399,4 +425,65 @@ describe SystemNoteService do
end
end
end
+
+ include JiraServiceHelper
+
+ describe 'JIRA integration' do
+ let(:project) { create(:project) }
+ 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_tracker) { project.create_jira_service if project.jira_service.nil? }
+ let(:commit) { project.commit }
+
+ context 'in JIRA issue tracker' do
+ before do
+ jira_service_settings
+ WebMock.stub_request(:post, jira_api_comment_url)
+ end
+
+ after do
+ jira_tracker.destroy!
+ end
+
+ describe "new reference" do
+ before do
+ WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
+ end
+
+ subject { described_class.cross_reference(jira_issue, commit, author) }
+
+ it { is_expected.to eq(jira_status_message) }
+ end
+
+ describe "existing reference" do
+ before do
+ message = "[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]."
+ WebMock.stub_request(:get, jira_api_comment_url).to_return(body: "{\"comments\":[{\"body\":\"#{message}\"}]}")
+ end
+
+ subject { described_class.cross_reference(jira_issue, commit, author) }
+ it { is_expected.not_to eq(jira_status_message) }
+ end
+ end
+
+ context 'issue from an issue' do
+ context 'in JIRA issue tracker' do
+ before do
+ jira_service_settings
+ WebMock.stub_request(:post, jira_api_comment_url)
+ WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
+ end
+
+ after do
+ jira_tracker.destroy!
+ end
+
+ subject { described_class.cross_reference(jira_issue, issue, author) }
+
+ it { is_expected.to eq(jira_status_message) }
+ end
+ end
+ end
end
diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb
index 226196eedae..f034f251ba4 100644
--- a/spec/services/test_hook_service_spec.rb
+++ b/spec/services/test_hook_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe TestHookService do
+describe TestHookService, services: true do
let(:user) { create :user }
let(:project) { create :project }
let(:hook) { create :project_hook, project: project }
diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb
new file mode 100644
index 00000000000..bba211089a8
--- /dev/null
+++ b/spec/services/update_release_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe UpdateReleaseService, services: true do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+ let(:new_description) { 'The best release!' }
+ let(:service) { UpdateReleaseService.new(project, user) }
+
+ context 'with an existing release' do
+ let(:create_service) { CreateReleaseService.new(project, user) }
+
+ before do
+ create_service.execute(tag_name, description)
+ end
+
+ it 'successfully updates an existing release' do
+ result = service.execute(tag_name, new_description)
+ expect(result[:status]).to eq(:success)
+ expect(project.releases.find_by(tag: tag_name).description).to eq(new_description)
+ end
+ end
+
+ it 'raises an error if the tag does not exist' do
+ result = service.execute("foobar", description)
+ expect(result[:status]).to eq(:error)
+ end
+
+ it 'raises an error if the release does not exist' do
+ result = service.execute(tag_name, description)
+ expect(result[:status]).to eq(:error)
+ end
+end
diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb
index d7c516e3934..48d114896d0 100644
--- a/spec/services/update_snippet_service_spec.rb
+++ b/spec/services/update_snippet_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe UpdateSnippetService do
+describe UpdateSnippetService, services: true do
before do
@user = create :user
@admin = create :user, admin: true
@@ -42,7 +42,7 @@ describe UpdateSnippetService do
CreateSnippetService.new(project, user, opts).execute
end
- def update_snippet(project = nil, user, snippet, opts)
+ def update_snippet(project, user, snippet, opts)
UpdateSnippetService.new(project, user, snippet, opts).execute
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2be13bb3e6a..0225a0ee53f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -31,6 +31,7 @@ RSpec.configure do |config|
config.include StubConfiguration
config.include RelativeUrl, type: feature
config.include TestEnv
+ config.include ActiveJob::TestHelper
config.include StubGitlabCalls
config.include StubGitlabData
config.include BenchmarkMatchers, benchmark: true
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index 97e5c270a59..d6e03cbef3d 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -1,4 +1,4 @@
-# Helper methods for Gitlab::Markdown filter specs
+# Helper methods for Banzai filter specs
#
# Must be included into specs manually
module FilterSpecHelper
@@ -10,36 +10,49 @@ module FilterSpecHelper
# if none is provided.
#
# html - HTML String to pass to the filter's `call` method.
- # contexts - Hash context for the filter. (default: {project: project})
+ # context - Hash context for the filter. (default: {project: project})
#
# Returns a Nokogiri::XML::DocumentFragment
- def filter(html, contexts = {})
+ def filter(html, context = {})
if defined?(project)
- contexts.reverse_merge!(project: project)
+ context.reverse_merge!(project: project)
end
- described_class.call(html, contexts)
+ described_class.call(html, context)
end
# Run text through HTML::Pipeline with the current filter and return the
# result Hash
#
# body - String text to run through the pipeline
- # contexts - Hash context for the filter. (default: {project: project})
+ # context - Hash context for the filter. (default: {project: project})
#
# Returns the Hash
- def pipeline_result(body, contexts = {})
- contexts.reverse_merge!(project: project) if defined?(project)
+ def pipeline_result(body, context = {})
+ context.reverse_merge!(project: project) if defined?(project)
- pipeline = HTML::Pipeline.new([described_class], contexts)
+ pipeline = HTML::Pipeline.new([described_class], context)
pipeline.call(body)
end
- def reference_pipeline_result(body, contexts = {})
- contexts.reverse_merge!(project: project) if defined?(project)
+ def reference_pipeline(context = {})
+ context.reverse_merge!(project: project) if defined?(project)
- pipeline = HTML::Pipeline.new([described_class, Gitlab::Markdown::ReferenceGathererFilter], contexts)
- pipeline.call(body)
+ filters = [
+ Banzai::Filter::AutolinkFilter,
+ described_class,
+ Banzai::Filter::ReferenceGathererFilter
+ ]
+
+ HTML::Pipeline.new(filters, context)
+ end
+
+ def reference_pipeline_result(body, context = {})
+ reference_pipeline(context).call(body)
+ end
+
+ def reference_filter(html, context = {})
+ reference_pipeline(context).to_document(html)
end
# Modify a String reference to make it invalid
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
new file mode 100644
index 00000000000..a3f496359b1
--- /dev/null
+++ b/spec/support/jira_service_helper.rb
@@ -0,0 +1,67 @@
+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"
+ }
+
+ jira_tracker.update_attributes(properties: properties, active: true)
+ end
+
+ def jira_status_message
+ "JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}."
+ end
+
+ def jira_issue_comments
+ "{\"startAt\":0,\"maxResults\":11,\"total\":11,
+ \"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\",
+ \"id\":\"10609\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
+ \"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},
+ \"displayName\":\"GitLab\",\"active\":true},
+ \"body\":\"[Administrator|http://localhost:3000/u/root] mentioned JIRA-1 in Merge request of [gitlab-org/gitlab-test|http://localhost:3000/gitlab-org/gitlab-test/merge_requests/2].\",
+ \"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
+ \"created\":\"2015-02-12T22:47:07.826+0100\",
+ \"updated\":\"2015-02-12T22:47:07.826+0100\"},
+ {\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10700\",
+ \"id\":\"10700\",\"author\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",
+ \"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
+ \"body\":\"[Administrator|http://localhost:3000/u/root] mentioned this issue in [a commit of h5bp/html5-boilerplate|http://localhost:3000/h5bp/html5-boilerplate/commit/2439f77897122fbeee3bfd9bb692d3608848433e].\",
+ \"updateAuthor\":{\"self\":\"http://0.0.0.0:4567/rest/api/2/user?username=gitlab\",\"name\":\"gitlab\",\"emailAddress\":\"gitlab@example.com\",
+ \"avatarUrls\":{\"16x16\":\"http://0.0.0.0:4567/secure/useravatar?size=xsmall&avatarId=10122\",
+ \"24x24\":\"http://0.0.0.0:4567/secure/useravatar?size=small&avatarId=10122\",
+ \"32x32\":\"http://0.0.0.0:4567/secure/useravatar?size=medium&avatarId=10122\",
+ \"48x48\":\"http://0.0.0.0:4567/secure/useravatar?avatarId=10122\"},\"displayName\":\"GitLab\",\"active\":true},
+ \"created\":\"2015-04-01T03:45:55.667+0200\",
+ \"updated\":\"2015-04-01T03:45:55.667+0200\"
+ }
+ ]}"
+ end
+
+ def jira_api_comment_url
+ 'http://jira.example/rest/api/2/issue/JIRA-1/comment'
+ end
+
+ def jira_api_transition_url
+ 'http://jira.example/rest/api/2/issue/JIRA-1/transitions'
+ end
+
+ def jira_api_test_url
+ 'http://jira.example/rest/api/2/myself'
+ end
+end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index bedc1a7f1db..d6d3062a197 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -93,6 +93,10 @@ class MarkdownFeature
end
end
+ def urls
+ Gitlab::Application.routes.url_helpers
+ end
+
def raw_markdown
markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb'))
ERB.new(markdown).result(binding)
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7500d0fdf80..7eadcd58c1f 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -71,7 +71,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-project_member', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-project_member', count: 4)
end
end
@@ -80,7 +80,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-issue', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-issue', count: 6)
end
end
@@ -89,7 +89,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-merge_request', count: 6)
expect(actual).to have_selector('em a.gfm-merge_request')
end
end
@@ -99,7 +99,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-snippet', count: 2)
+ expect(actual).to have_selector('a.gfm.gfm-snippet', count: 5)
end
end
@@ -108,7 +108,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 2)
+ expect(actual).to have_selector('a.gfm.gfm-commit_range', count: 5)
end
end
@@ -117,7 +117,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-commit', count: 2)
+ expect(actual).to have_selector('a.gfm.gfm-commit', count: 5)
end
end
@@ -126,7 +126,7 @@ module MarkdownMatchers
set_default_markdown_messages
match do |actual|
- expect(actual).to have_selector('a.gfm.gfm-label', count: 3)
+ expect(actual).to have_selector('a.gfm.gfm-label', count: 4)
end
end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index 3bb568f4d49..fce91015fd4 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -4,18 +4,18 @@
# - let(:backref_text) { "the way that +subject+ should refer to itself in backreferences " }
# - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } }
-def common_mentionable_setup
+shared_context 'mentionable context' do
let(:project) { subject.project }
let(:author) { subject.author }
let(:mentioned_issue) { create(:issue, project: project) }
let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) }
- let(:mentioned_commit) { project.commit }
+ let(:mentioned_commit) { project.commit("HEAD~1") }
let(:ext_proj) { create(:project, :public) }
let(:ext_issue) { create(:issue, project: ext_proj) }
let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) }
- let(:ext_commit) { ext_proj.commit }
+ let(:ext_commit) { ext_proj.commit("HEAD~2") }
# Override to add known commits to the repository stub.
let(:extra_commits) { [] }
@@ -45,21 +45,18 @@ def common_mentionable_setup
before do
# Wire the project's repository to return the mentioned commit, and +nil+
# for any unrecognized commits.
- commitmap = {
- mentioned_commit.id => mentioned_commit
- }
- extra_commits.each { |c| commitmap[c.short_id] = c }
-
- allow(Project).to receive(:find).and_call_original
- allow(Project).to receive(:find).with(project.id.to_s).and_return(project)
- allow(project.repository).to receive(:commit) { |sha| commitmap[sha] }
+ allow_any_instance_of(::Repository).to receive(:commit).and_call_original
+ allow_any_instance_of(::Repository).to receive(:commit).with(mentioned_commit.short_id).and_return(mentioned_commit)
+ extra_commits.each do |commit|
+ allow_any_instance_of(::Repository).to receive(:commit).with(commit.short_id).and_return(commit)
+ end
set_mentionable_text.call(ref_string)
end
end
shared_examples 'a mentionable' do
- common_mentionable_setup
+ include_context 'mentionable context'
it 'generates a descriptive back-reference' do
expect(subject.gfm_reference).to eq(backref_text)
@@ -91,7 +88,7 @@ shared_examples 'a mentionable' do
end
shared_examples 'an editable mentionable' do
- common_mentionable_setup
+ include_context 'mentionable context'
it_behaves_like 'a mentionable'
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index aadf791bf3f..aa8258d6dad 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -45,12 +45,12 @@ eos
def another_sample_commit
OpenStruct.new(
- id: "e56497bb5f03a90a51293fc6d516788730953899",
- parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40',
- author_full_name: "Sytse Sijbrandij",
- author_email: "sytse@gitlab.com",
- files_changed_count: 1,
- message: <<eos
+ id: "e56497bb5f03a90a51293fc6d516788730953899",
+ parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40',
+ author_full_name: "Sytse Sijbrandij",
+ author_email: "sytse@gitlab.com",
+ files_changed_count: 1,
+ message: <<eos
Add directory structure for tree_helper spec
This directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
index 5b3eb1bfc5f..eec2e681117 100644
--- a/spec/support/stub_gitlab_calls.rb
+++ b/spec/support/stub_gitlab_calls.rb
@@ -21,6 +21,10 @@ module StubGitlabCalls
allow_any_instance_of(Ci::Commit).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
+
private
def gitlab_url
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index d12ba25b71b..4f4743bff6d 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -12,6 +12,7 @@ module TestEnv
'fix' => '48f0be4',
'improve/awesome' => '5937ac0',
'markdown' => '0ed8c6c',
+ 'lfs' => 'be93687',
'master' => '5937ac0',
"'test'" => 'e56497b',
}
@@ -21,7 +22,8 @@ module TestEnv
# We currently only need a subset of the branches
FORKED_BRANCH_SHA = {
'add-submodule-version-bump' => '3f547c08',
- 'master' => '5937ac0'
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c0'
}
# Test environment
@@ -96,15 +98,15 @@ module TestEnv
clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
unless File.directory?(repo_path)
- system(*%W(git clone -q #{clone_url} #{repo_path}))
+ system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
end
Dir.chdir(repo_path) do
branch_sha.each do |branch, sha|
# Try to reset without fetching to avoid using the network.
- reset = %W(git update-ref refs/heads/#{branch} #{sha})
+ reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
unless system(*reset)
- if system(*%w(git fetch origin))
+ if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
unless system(*reset)
raise 'The fetched test seed '\
'does not contain the required revision.'
@@ -117,7 +119,7 @@ module TestEnv
end
# We must copy bare repositories because we will push to them.
- system(git_env, *%W(git clone -q --bare #{repo_path} #{repo_path_bare}))
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
end
def copy_repo(project)
diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb
new file mode 100644
index 00000000000..692d219e9f1
--- /dev/null
+++ b/spec/support/wait_for_ajax.rb
@@ -0,0 +1,11 @@
+module WaitForAjax
+ def wait_for_ajax
+ Timeout.timeout(Capybara.default_wait_time) do
+ loop until finished_all_ajax_requests?
+ end
+ end
+
+ def finished_all_ajax_requests?
+ page.evaluate_script('jQuery.active').zero?
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 3be7dd4e52b..63bed2414df 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads builds}.each do |subtask|
+ %w{db repo uploads builds artifacts lfs}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -49,12 +49,15 @@ describe 'gitlab:app namespace rake task' do
to raise_error(SystemExit)
end
- it 'should invoke restoration on mach' do
+ it 'should invoke restoration on match' do
allow(YAML).to receive(:load_file).
and_return({ gitlab_version: gitlab_version })
expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:uploads:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
@@ -112,19 +115,21 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories builds}
+ %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/')
+ expect(tar_contents).to match('uploads.tar.gz')
expect(tar_contents).to match('repositories/')
- expect(tar_contents).to match('builds/')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories|builds)\/$/)
+ 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)\/$/)
end
it 'should delete temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds,artifacts,lfs}')
)
expect(temp_dirs).to be_empty
@@ -146,7 +151,7 @@ describe 'gitlab:app namespace rake task' do
# Redirect STDOUT and run the rake task
orig_stdout = $stdout
$stdout = StringIO.new
- ENV["SKIP"] = "repositories"
+ ENV["SKIP"] = "repositories,uploads"
run_rake_task('gitlab:backup:create')
$stdout = orig_stdout
@@ -160,12 +165,14 @@ 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 repositories builds}
+ %W{tar -tvf #{@backup_tar} db uploads.tar.gz repositories builds.tar.gz artifacts.tar.gz lfs.tar.gz}
)
expect(tar_contents).to match('db/')
- expect(tar_contents).to match('uploads/')
- expect(tar_contents).to match('builds/')
+ expect(tar_contents).to match('uploads.tar.gz')
+ 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('repositories/')
end
@@ -176,7 +183,10 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
+ expect(Rake::Task["gitlab:backup:uploads:restore"]).not_to receive :invoke
expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
+ expect(Rake::Task["gitlab:backup:artifacts:restore"]).to receive :invoke
+ expect(Rake::Task["gitlab:backup:lfs:restore"]).to receive :invoke
expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
new file mode 100644
index 00000000000..98deae0a588
--- /dev/null
+++ b/spec/workers/build_email_worker_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe BuildEmailWorker do
+ include RepoHelpers
+
+ let(:build) { create(:ci_build) }
+ let(:user) { create(:user) }
+ let(:data) { Gitlab::BuildDataBuilder.build(build) }
+
+ subject { BuildEmailWorker.new }
+
+ before do
+ allow(build).to receive(:execute_hooks).and_return(false)
+ build.success
+ end
+
+ describe "#perform" do
+ it "sends mail" do
+ subject.perform(build.id, [user.email], data.stringify_keys)
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email.subject).to include('Build success for')
+ expect(email.to).to eq([user.email])
+ end
+
+ it "gracefully handles an input SMTP error" do
+ ActionMailer::Base.deliveries.clear
+ allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
+
+ subject.perform(build.id, [user.email], data.stringify_keys)
+
+ expect(ActionMailer::Base.deliveries.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index 65a8d7d9197..de40a6f78af 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -21,12 +21,14 @@ describe EmailReceiverWorker do
end
it "sends out a rejection email" do
- described_class.new.perform(raw_message)
-
- email = ActionMailer::Base.deliveries.last
- expect(email).not_to be_nil
- expect(email.to).to eq(["jake@adventuretime.ooo"])
- expect(email.subject).to include("Rejected")
+ perform_enqueued_jobs do
+ described_class.new.perform(raw_message)
+
+ email = ActionMailer::Base.deliveries.last
+ expect(email).not_to be_nil
+ expect(email.to).to eq(["jake@adventuretime.ooo"])
+ expect(email.subject).to include("Rejected")
+ end
end
end
end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index aa031106968..dae31992620 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -9,21 +9,22 @@ describe RepositoryForkWorker do
describe "#perform" do
it "creates a new repository from a fork" do
expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).with(
- project.path_with_namespace,
- fork_project.namespace.path).
- and_return(true)
- expect(ProjectCacheWorker).to receive(:perform_async)
+ project.path_with_namespace,
+ fork_project.namespace.path
+ ).and_return(true)
- subject.perform(project.id,
- project.path_with_namespace,
- fork_project.namespace.path)
+ subject.perform(
+ project.id,
+ project.path_with_namespace,
+ fork_project.namespace.path)
end
it "handles bad fork" do
expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false)
- subject.perform(project.id,
- project.path_with_namespace,
- fork_project.namespace.path)
+ subject.perform(
+ project.id,
+ project.path_with_namespace,
+ fork_project.namespace.path)
end
end
end
diff --git a/spec/workers/stuck_ci_builds_worker_spec.rb b/spec/workers/stuck_ci_builds_worker_spec.rb
new file mode 100644
index 00000000000..665ec20f224
--- /dev/null
+++ b/spec/workers/stuck_ci_builds_worker_spec.rb
@@ -0,0 +1,44 @@
+require "spec_helper"
+
+describe StuckCiBuildsWorker do
+ let!(:build) { create :ci_build }
+
+ subject do
+ build.reload
+ build.status
+ end
+
+ %w(pending running).each do |status|
+ context "#{status} build" do
+ before do
+ build.update!(status: status)
+ end
+
+ it 'gets dropped if it was updated over 2 days ago' do
+ build.update!(updated_at: 2.days.ago)
+ StuckCiBuildsWorker.new.perform
+ is_expected.to eq('failed')
+ end
+
+ it "is still #{status}" do
+ build.update!(updated_at: 1.minute.ago)
+ StuckCiBuildsWorker.new.perform
+ is_expected.to eq(status)
+ end
+ end
+ end
+
+ %w(success failed canceled).each do |status|
+ context "#{status} build" do
+ before do
+ build.update!(status: status)
+ end
+
+ it "is still #{status}" do
+ build.update!(updated_at: 2.days.ago)
+ StuckCiBuildsWorker.new.perform
+ is_expected.to eq(status)
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/clipboard.js b/vendor/assets/javascripts/clipboard.js
new file mode 100644
index 00000000000..1b1f4f0bd63
--- /dev/null
+++ b/vendor/assets/javascripts/clipboard.js
@@ -0,0 +1,621 @@
+/*!
+ * clipboard.js v1.4.2
+ * https://zenorocha.github.io/clipboard.js
+ *
+ * Licensed MIT © Zeno Rocha
+ */
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/**
+ * Module dependencies.
+ */
+
+var closest = require('closest')
+ , event = require('component-event');
+
+/**
+ * Delegate event `type` to `selector`
+ * and invoke `fn(e)`. A callback function
+ * is returned which may be passed to `.unbind()`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+// Some events don't bubble, so we want to bind to the capture phase instead
+// when delegating.
+var forceCaptureEvents = ['focus', 'blur'];
+
+exports.bind = function(el, selector, type, fn, capture){
+ if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
+
+ return event.bind(el, type, function(e){
+ var target = e.target || e.srcElement;
+ e.delegateTarget = closest(target, selector, true, el);
+ if (e.delegateTarget) fn.call(el, e);
+ }, capture);
+};
+
+/**
+ * Unbind event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ if (forceCaptureEvents.indexOf(type) !== -1) capture = true;
+
+ event.unbind(el, type, fn, capture);
+};
+
+},{"closest":2,"component-event":4}],2:[function(require,module,exports){
+var matches = require('matches-selector')
+
+module.exports = function (element, selector, checkYoSelf) {
+ var parent = checkYoSelf ? element : element.parentNode
+
+ while (parent && parent !== document) {
+ if (matches(parent, selector)) return parent;
+ parent = parent.parentNode
+ }
+}
+
+},{"matches-selector":3}],3:[function(require,module,exports){
+
+/**
+ * Element prototype.
+ */
+
+var proto = Element.prototype;
+
+/**
+ * Vendor function.
+ */
+
+var vendor = proto.matchesSelector
+ || proto.webkitMatchesSelector
+ || proto.mozMatchesSelector
+ || proto.msMatchesSelector
+ || proto.oMatchesSelector;
+
+/**
+ * Expose `match()`.
+ */
+
+module.exports = match;
+
+/**
+ * Match `el` to `selector`.
+ *
+ * @param {Element} el
+ * @param {String} selector
+ * @return {Boolean}
+ * @api public
+ */
+
+function match(el, selector) {
+ if (vendor) return vendor.call(el, selector);
+ var nodes = el.parentNode.querySelectorAll(selector);
+ for (var i = 0; i < nodes.length; ++i) {
+ if (nodes[i] == el) return true;
+ }
+ return false;
+}
+},{}],4:[function(require,module,exports){
+var bind = window.addEventListener ? 'addEventListener' : 'attachEvent',
+ unbind = window.removeEventListener ? 'removeEventListener' : 'detachEvent',
+ prefix = bind !== 'addEventListener' ? 'on' : '';
+
+/**
+ * Bind `el` event `type` to `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.bind = function(el, type, fn, capture){
+ el[bind](prefix + type, fn, capture || false);
+ return fn;
+};
+
+/**
+ * Unbind `el` event `type`'s callback `fn`.
+ *
+ * @param {Element} el
+ * @param {String} type
+ * @param {Function} fn
+ * @param {Boolean} capture
+ * @return {Function}
+ * @api public
+ */
+
+exports.unbind = function(el, type, fn, capture){
+ el[unbind](prefix + type, fn, capture || false);
+ return fn;
+};
+},{}],5:[function(require,module,exports){
+function E () {
+ // Keep this empty so it's easier to inherit from
+ // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)
+}
+
+E.prototype = {
+ on: function (name, callback, ctx) {
+ var e = this.e || (this.e = {});
+
+ (e[name] || (e[name] = [])).push({
+ fn: callback,
+ ctx: ctx
+ });
+
+ return this;
+ },
+
+ once: function (name, callback, ctx) {
+ var self = this;
+ var fn = function () {
+ self.off(name, fn);
+ callback.apply(ctx, arguments);
+ };
+
+ return this.on(name, fn, ctx);
+ },
+
+ emit: function (name) {
+ var data = [].slice.call(arguments, 1);
+ var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
+ var i = 0;
+ var len = evtArr.length;
+
+ for (i; i < len; i++) {
+ evtArr[i].fn.apply(evtArr[i].ctx, data);
+ }
+
+ return this;
+ },
+
+ off: function (name, callback) {
+ var e = this.e || (this.e = {});
+ var evts = e[name];
+ var liveEvents = [];
+
+ if (evts && callback) {
+ for (var i = 0, len = evts.length; i < len; i++) {
+ if (evts[i].fn !== callback) liveEvents.push(evts[i]);
+ }
+ }
+
+ // Remove event from queue to prevent memory leak
+ // Suggested by https://github.com/lazd
+ // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910
+
+ (liveEvents.length)
+ ? e[name] = liveEvents
+ : delete e[name];
+
+ return this;
+ }
+};
+
+module.exports = E;
+
+},{}],6:[function(require,module,exports){
+/**
+ * Inner class which performs selection from either `text` or `target`
+ * properties and then executes copy or cut operations.
+ */
+'use strict';
+
+exports.__esModule = true;
+
+var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+var ClipboardAction = (function () {
+ /**
+ * @param {Object} options
+ */
+
+ function ClipboardAction(options) {
+ _classCallCheck(this, ClipboardAction);
+
+ this.resolveOptions(options);
+ this.initSelection();
+ }
+
+ /**
+ * Defines base properties passed from constructor.
+ * @param {Object} options
+ */
+
+ ClipboardAction.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = options.action;
+ this.emitter = options.emitter;
+ this.target = options.target;
+ this.text = options.text;
+ this.trigger = options.trigger;
+
+ this.selectedText = '';
+ };
+
+ /**
+ * Decides which selection strategy is going to be applied based
+ * on the existence of `text` and `target` properties.
+ */
+
+ ClipboardAction.prototype.initSelection = function initSelection() {
+ if (this.text && this.target) {
+ throw new Error('Multiple attributes declared, use either "target" or "text"');
+ } else if (this.text) {
+ this.selectFake();
+ } else if (this.target) {
+ this.selectTarget();
+ } else {
+ throw new Error('Missing required attributes, use either "target" or "text"');
+ }
+ };
+
+ /**
+ * Creates a fake textarea element, sets its value from `text` property,
+ * and makes a selection on it.
+ */
+
+ ClipboardAction.prototype.selectFake = function selectFake() {
+ var _this = this;
+
+ this.removeFake();
+
+ this.fakeHandler = document.body.addEventListener('click', function () {
+ return _this.removeFake();
+ });
+
+ this.fakeElem = document.createElement('textarea');
+ this.fakeElem.style.position = 'absolute';
+ this.fakeElem.style.left = '-9999px';
+ this.fakeElem.style.top = (window.pageYOffset || document.documentElement.scrollTop) + 'px';
+ this.fakeElem.setAttribute('readonly', '');
+ this.fakeElem.value = this.text;
+ this.selectedText = this.text;
+
+ document.body.appendChild(this.fakeElem);
+
+ this.fakeElem.select();
+ this.copyText();
+ };
+
+ /**
+ * Only removes the fake element after another click event, that way
+ * a user can hit `Ctrl+C` to copy because selection still exists.
+ */
+
+ ClipboardAction.prototype.removeFake = function removeFake() {
+ if (this.fakeHandler) {
+ document.body.removeEventListener('click');
+ this.fakeHandler = null;
+ }
+
+ if (this.fakeElem) {
+ document.body.removeChild(this.fakeElem);
+ this.fakeElem = null;
+ }
+ };
+
+ /**
+ * Selects the content from element passed on `target` property.
+ */
+
+ ClipboardAction.prototype.selectTarget = function selectTarget() {
+ if (this.target.nodeName === 'INPUT' || this.target.nodeName === 'TEXTAREA') {
+ this.target.select();
+ this.selectedText = this.target.value;
+ } else {
+ var range = document.createRange();
+ var selection = window.getSelection();
+
+ selection.removeAllRanges();
+ range.selectNodeContents(this.target);
+ selection.addRange(range);
+ this.selectedText = selection.toString();
+ }
+
+ this.copyText();
+ };
+
+ /**
+ * Executes the copy operation based on the current selection.
+ */
+
+ ClipboardAction.prototype.copyText = function copyText() {
+ var succeeded = undefined;
+
+ try {
+ succeeded = document.execCommand(this.action);
+ } catch (err) {
+ succeeded = false;
+ }
+
+ this.handleResult(succeeded);
+ };
+
+ /**
+ * Fires an event based on the copy operation result.
+ * @param {Boolean} succeeded
+ */
+
+ ClipboardAction.prototype.handleResult = function handleResult(succeeded) {
+ if (succeeded) {
+ this.emitter.emit('success', {
+ action: this.action,
+ text: this.selectedText,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ } else {
+ this.emitter.emit('error', {
+ action: this.action,
+ trigger: this.trigger,
+ clearSelection: this.clearSelection.bind(this)
+ });
+ }
+ };
+
+ /**
+ * Removes current selection and focus from `target` element.
+ */
+
+ ClipboardAction.prototype.clearSelection = function clearSelection() {
+ if (this.target) {
+ this.target.blur();
+ }
+
+ window.getSelection().removeAllRanges();
+ };
+
+ /**
+ * Sets the `action` to be performed which can be either 'copy' or 'cut'.
+ * @param {String} action
+ */
+
+ /**
+ * Destroy lifecycle.
+ */
+
+ ClipboardAction.prototype.destroy = function destroy() {
+ this.removeFake();
+ };
+
+ _createClass(ClipboardAction, [{
+ key: 'action',
+ set: function set() {
+ var action = arguments.length <= 0 || arguments[0] === undefined ? 'copy' : arguments[0];
+
+ this._action = action;
+
+ if (this._action !== 'copy' && this._action !== 'cut') {
+ throw new Error('Invalid "action" value, use either "copy" or "cut"');
+ }
+ },
+
+ /**
+ * Gets the `action` property.
+ * @return {String}
+ */
+ get: function get() {
+ return this._action;
+ }
+
+ /**
+ * Sets the `target` property using an element
+ * that will be have its content copied.
+ * @param {Element} target
+ */
+ }, {
+ key: 'target',
+ set: function set(target) {
+ if (target !== undefined) {
+ if (target && typeof target === 'object' && target.nodeType === 1) {
+ this._target = target;
+ } else {
+ throw new Error('Invalid "target" value, use a valid Element');
+ }
+ }
+ },
+
+ /**
+ * Gets the `target` property.
+ * @return {String|HTMLElement}
+ */
+ get: function get() {
+ return this._target;
+ }
+ }]);
+
+ return ClipboardAction;
+})();
+
+exports['default'] = ClipboardAction;
+module.exports = exports['default'];
+
+},{}],7:[function(require,module,exports){
+'use strict';
+
+exports.__esModule = true;
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var _clipboardAction = require('./clipboard-action');
+
+var _clipboardAction2 = _interopRequireDefault(_clipboardAction);
+
+var _delegateEvents = require('delegate-events');
+
+var _delegateEvents2 = _interopRequireDefault(_delegateEvents);
+
+var _tinyEmitter = require('tiny-emitter');
+
+var _tinyEmitter2 = _interopRequireDefault(_tinyEmitter);
+
+/**
+ * Base class which takes a selector, delegates a click event to it,
+ * and instantiates a new `ClipboardAction` on each click.
+ */
+
+var Clipboard = (function (_Emitter) {
+ _inherits(Clipboard, _Emitter);
+
+ /**
+ * @param {String} selector
+ * @param {Object} options
+ */
+
+ function Clipboard(selector, options) {
+ _classCallCheck(this, Clipboard);
+
+ _Emitter.call(this);
+
+ this.resolveOptions(options);
+ this.delegateClick(selector);
+ }
+
+ /**
+ * Helper function to retrieve attribute value.
+ * @param {String} suffix
+ * @param {Element} element
+ */
+
+ /**
+ * Defines if attributes would be resolved using internal setter functions
+ * or custom functions that were passed in the constructor.
+ * @param {Object} options
+ */
+
+ Clipboard.prototype.resolveOptions = function resolveOptions() {
+ var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0];
+
+ this.action = typeof options.action === 'function' ? options.action : this.defaultAction;
+ this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;
+ this.text = typeof options.text === 'function' ? options.text : this.defaultText;
+ };
+
+ /**
+ * Delegates a click event on the passed selector.
+ * @param {String} selector
+ */
+
+ Clipboard.prototype.delegateClick = function delegateClick(selector) {
+ var _this = this;
+
+ this.binding = _delegateEvents2['default'].bind(document.body, selector, 'click', function (e) {
+ return _this.onClick(e);
+ });
+ };
+
+ /**
+ * Undelegates a click event on body.
+ * @param {String} selector
+ */
+
+ Clipboard.prototype.undelegateClick = function undelegateClick() {
+ _delegateEvents2['default'].unbind(document.body, 'click', this.binding);
+ };
+
+ /**
+ * Defines a new `ClipboardAction` on each click event.
+ * @param {Event} e
+ */
+
+ Clipboard.prototype.onClick = function onClick(e) {
+ if (this.clipboardAction) {
+ this.clipboardAction = null;
+ }
+
+ this.clipboardAction = new _clipboardAction2['default']({
+ action: this.action(e.delegateTarget),
+ target: this.target(e.delegateTarget),
+ text: this.text(e.delegateTarget),
+ trigger: e.delegateTarget,
+ emitter: this
+ });
+ };
+
+ /**
+ * Default `action` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultAction = function defaultAction(trigger) {
+ return getAttributeValue('action', trigger);
+ };
+
+ /**
+ * Default `target` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultTarget = function defaultTarget(trigger) {
+ var selector = getAttributeValue('target', trigger);
+
+ if (selector) {
+ return document.querySelector(selector);
+ }
+ };
+
+ /**
+ * Default `text` lookup function.
+ * @param {Element} trigger
+ */
+
+ Clipboard.prototype.defaultText = function defaultText(trigger) {
+ return getAttributeValue('text', trigger);
+ };
+
+ /**
+ * Destroy lifecycle.
+ */
+
+ Clipboard.prototype.destroy = function destroy() {
+ this.undelegateClick();
+
+ if (this.clipboardAction) {
+ this.clipboardAction.destroy();
+ this.clipboardAction = null;
+ }
+ };
+
+ return Clipboard;
+})(_tinyEmitter2['default']);
+
+function getAttributeValue(suffix, element) {
+ var attribute = 'data-clipboard-' + suffix;
+
+ if (!element.hasAttribute(attribute)) {
+ return;
+ }
+
+ return element.getAttribute(attribute);
+}
+
+exports['default'] = Clipboard;
+module.exports = exports['default'];
+
+},{"./clipboard-action":6,"delegate-events":1,"tiny-emitter":5}]},{},[7])(7)
+}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js
deleted file mode 100644
index c8702d79b65..00000000000
--- a/vendor/assets/javascripts/jquery.blockUI.js
+++ /dev/null
@@ -1,590 +0,0 @@
-/*!
- * jQuery blockUI plugin
- * Version 2.60.0-2013.04.05
- * @requires jQuery v1.7 or later
- *
- * Examples at: http://malsup.com/jquery/block/
- * Copyright (c) 2007-2013 M. Alsup
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- * Thanks to Amir-Hossein Sobhi for some excellent contributions!
- */
-
-;(function() {
-/*jshint eqeqeq:false curly:false latedef:false */
-"use strict";
-
- function setup($) {
- $.fn._fadeIn = $.fn.fadeIn;
-
- var noOp = $.noop || function() {};
-
- // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
- // retarded userAgent strings on Vista)
- var msie = /MSIE/.test(navigator.userAgent);
- var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent);
- var mode = document.documentMode || 0;
- var setExpr = $.isFunction( document.createElement('div').style.setExpression );
-
- // global $ methods for blocking/unblocking the entire page
- $.blockUI = function(opts) { install(window, opts); };
- $.unblockUI = function(opts) { remove(window, opts); };
-
- // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl)
- $.growlUI = function(title, message, timeout, onClose) {
- var $m = $('<div class="growlUI"></div>');
- if (title) $m.append('<h1>'+title+'</h1>');
- if (message) $m.append('<h2>'+message+'</h2>');
- if (timeout === undefined) timeout = 3000;
- $.blockUI({
- message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
- timeout: timeout, showOverlay: false,
- onUnblock: onClose,
- css: $.blockUI.defaults.growlCSS
- });
- };
-
- // plugin method for blocking element content
- $.fn.block = function(opts) {
- if ( this[0] === window ) {
- $.blockUI( opts );
- return this;
- }
- var fullOpts = $.extend({}, $.blockUI.defaults, opts || {});
- this.each(function() {
- var $el = $(this);
- if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked'))
- return;
- $el.unblock({ fadeOut: 0 });
- });
-
- return this.each(function() {
- if ($.css(this,'position') == 'static') {
- this.style.position = 'relative';
- $(this).data('blockUI.static', true);
- }
- this.style.zoom = 1; // force 'hasLayout' in ie
- install(this, opts);
- });
- };
-
- // plugin method for unblocking element content
- $.fn.unblock = function(opts) {
- if ( this[0] === window ) {
- $.unblockUI( opts );
- return this;
- }
- return this.each(function() {
- remove(this, opts);
- });
- };
-
- $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost!
-
- // override these in your code to change the default behavior and style
- $.blockUI.defaults = {
- // message displayed when blocking (use null for no message)
- message: '<h1>Please wait...</h1>',
-
- title: null, // title string; only used when theme == true
- draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded)
-
- theme: false, // set to true to use with jQuery UI themes
-
- // styles for the message when blocking; if you wish to disable
- // these and use an external stylesheet then do this in your code:
- // $.blockUI.defaults.css = {};
- css: {
- padding: 0,
- margin: 0,
- width: '30%',
- top: '40%',
- left: '35%',
- textAlign: 'center',
- color: '#000',
- border: '3px solid #aaa',
- backgroundColor:'#fff',
- cursor: 'wait'
- },
-
- // minimal style set used when themes are used
- themedCSS: {
- width: '30%',
- top: '40%',
- left: '35%'
- },
-
- // styles for the overlay
- overlayCSS: {
- backgroundColor: '#000',
- opacity: 0.6,
- cursor: 'wait'
- },
-
- // style to replace wait cursor before unblocking to correct issue
- // of lingering wait cursor
- cursorReset: 'default',
-
- // styles applied when using $.growlUI
- growlCSS: {
- width: '350px',
- top: '10px',
- left: '',
- right: '10px',
- border: 'none',
- padding: '5px',
- opacity: 0.6,
- cursor: 'default',
- color: '#fff',
- backgroundColor: '#000',
- '-webkit-border-radius':'10px',
- '-moz-border-radius': '10px',
- 'border-radius': '10px'
- },
-
- // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
- // (hat tip to Jorge H. N. de Vasconcelos)
- /*jshint scripturl:true */
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',
-
- // force usage of iframe in non-IE browsers (handy for blocking applets)
- forceIframe: false,
-
- // z-index for the blocking overlay
- baseZ: 1000,
-
- // set these to true to have the message automatically centered
- centerX: true, // <-- only effects element blocking (page block controlled via css above)
- centerY: true,
-
- // allow body element to be stetched in ie6; this makes blocking look better
- // on "short" pages. disable if you wish to prevent changes to the body height
- allowBodyStretch: true,
-
- // enable if you want key and mouse events to be disabled for content that is blocked
- bindEvents: true,
-
- // be default blockUI will supress tab navigation from leaving blocking content
- // (if bindEvents is true)
- constrainTabKey: true,
-
- // fadeIn time in millis; set to 0 to disable fadeIn on block
- fadeIn: 200,
-
- // fadeOut time in millis; set to 0 to disable fadeOut on unblock
- fadeOut: 400,
-
- // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
- timeout: 0,
-
- // disable if you don't want to show the overlay
- showOverlay: true,
-
- // if true, focus will be placed in the first available input field when
- // page blocking
- focusInput: true,
-
- // elements that can receive focus
- focusableElements: ':input:enabled:visible',
-
- // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
- // no longer needed in 2012
- // applyPlatformOpacityRules: true,
-
- // callback method invoked when fadeIn has completed and blocking message is visible
- onBlock: null,
-
- // callback method invoked when unblocking has completed; the callback is
- // passed the element that has been unblocked (which is the window object for page
- // blocks) and the options that were passed to the unblock call:
- // onUnblock(element, options)
- onUnblock: null,
-
- // callback method invoked when the overlay area is clicked.
- // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used.
- onOverlayClick: null,
-
- // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
- quirksmodeOffsetHack: 4,
-
- // class name of the message block
- blockMsgClass: 'blockMsg',
-
- // if it is already blocked, then ignore it (don't unblock and reblock)
- ignoreIfBlocked: false
- };
-
- // private data and functions follow...
-
- var pageBlock = null;
- var pageBlockEls = [];
-
- function install(el, opts) {
- var css, themedCSS;
- var full = (el == window);
- var msg = (opts && opts.message !== undefined ? opts.message : undefined);
- opts = $.extend({}, $.blockUI.defaults, opts || {});
-
- if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked'))
- return;
-
- opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
- css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
- if (opts.onOverlayClick)
- opts.overlayCSS.cursor = 'pointer';
-
- themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
- msg = msg === undefined ? opts.message : msg;
-
- // remove the current block (if there is one)
- if (full && pageBlock)
- remove(window, {fadeOut:0});
-
- // if an existing element is being used as the blocking content then we capture
- // its current place in the DOM (and current display style) so we can restore
- // it when we unblock
- if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
- var node = msg.jquery ? msg[0] : msg;
- var data = {};
- $(el).data('blockUI.history', data);
- data.el = node;
- data.parent = node.parentNode;
- data.display = node.style.display;
- data.position = node.style.position;
- if (data.parent)
- data.parent.removeChild(node);
- }
-
- $(el).data('blockUI.onUnblock', opts.onUnblock);
- var z = opts.baseZ;
-
- // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
- // layer1 is the iframe layer which is used to supress bleed through of underlying content
- // layer2 is the overlay layer which has opacity and a wait cursor (by default)
- // layer3 is the message content that is displayed while blocking
- var lyr1, lyr2, lyr3, s;
- if (msie || opts.forceIframe)
- lyr1 = $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>');
- else
- lyr1 = $('<div class="blockUI" style="display:none"></div>');
-
- if (opts.theme)
- lyr2 = $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>');
- else
- lyr2 = $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
-
- if (opts.theme && full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (opts.theme) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
- }
- else {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
- }
- lyr3 = $(s);
-
- // if we have a message, style it
- if (msg) {
- if (opts.theme) {
- lyr3.css(themedCSS);
- lyr3.addClass('ui-widget-content');
- }
- else
- lyr3.css(css);
- }
-
- // style the overlay
- if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/)
- lyr2.css(opts.overlayCSS);
- lyr2.css('position', full ? 'fixed' : 'absolute');
-
- // make iframe layer transparent in IE
- if (msie || opts.forceIframe)
- lyr1.css('opacity',0.0);
-
- //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
- var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el);
- $.each(layers, function() {
- this.appendTo($par);
- });
-
- if (opts.theme && opts.draggable && $.fn.draggable) {
- lyr3.draggable({
- handle: '.ui-dialog-titlebar',
- cancel: 'li'
- });
- }
-
- // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
- var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0);
- if (ie6 || expr) {
- // give body 100% height
- if (full && opts.allowBodyStretch && $.support.boxModel)
- $('html,body').css('height','100%');
-
- // fix ie6 issue when blocked element has a border width
- if ((ie6 || !$.support.boxModel) && !full) {
- var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth');
- var fixT = t ? '(0 - '+t+')' : 0;
- var fixL = l ? '(0 - '+l+')' : 0;
- }
-
- // simulate fixed position
- $.each(layers, function(i,o) {
- var s = o[0].style;
- s.position = 'absolute';
- if (i < 2) {
- if (full)
- s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"');
- else
- s.setExpression('height','this.parentNode.offsetHeight + "px"');
- if (full)
- s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"');
- else
- s.setExpression('width','this.parentNode.offsetWidth + "px"');
- if (fixL) s.setExpression('left', fixL);
- if (fixT) s.setExpression('top', fixT);
- }
- else if (opts.centerY) {
- if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
- s.marginTop = 0;
- }
- else if (!opts.centerY && full) {
- var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0;
- var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"';
- s.setExpression('top',expression);
- }
- });
- }
-
- // show the message
- if (msg) {
- if (opts.theme)
- lyr3.find('.ui-widget-content').append(msg);
- else
- lyr3.append(msg);
- if (msg.jquery || msg.nodeType)
- $(msg).show();
- }
-
- if ((msie || opts.forceIframe) && opts.showOverlay)
- lyr1.show(); // opacity is zero
- if (opts.fadeIn) {
- var cb = opts.onBlock ? opts.onBlock : noOp;
- var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
- var cb2 = msg ? cb : noOp;
- if (opts.showOverlay)
- lyr2._fadeIn(opts.fadeIn, cb1);
- if (msg)
- lyr3._fadeIn(opts.fadeIn, cb2);
- }
- else {
- if (opts.showOverlay)
- lyr2.show();
- if (msg)
- lyr3.show();
- if (opts.onBlock)
- opts.onBlock();
- }
-
- // bind key and mouse events
- bind(1, el, opts);
-
- if (full) {
- pageBlock = lyr3[0];
- pageBlockEls = $(opts.focusableElements,pageBlock);
- if (opts.focusInput)
- setTimeout(focus, 20);
- }
- else
- center(lyr3[0], opts.centerX, opts.centerY);
-
- if (opts.timeout) {
- // auto-unblock
- var to = setTimeout(function() {
- if (full)
- $.unblockUI(opts);
- else
- $(el).unblock(opts);
- }, opts.timeout);
- $(el).data('blockUI.timeout', to);
- }
- }
-
- // remove the block
- function remove(el, opts) {
- var count;
- var full = (el == window);
- var $el = $(el);
- var data = $el.data('blockUI.history');
- var to = $el.data('blockUI.timeout');
- if (to) {
- clearTimeout(to);
- $el.removeData('blockUI.timeout');
- }
- opts = $.extend({}, $.blockUI.defaults, opts || {});
- bind(0, el, opts); // unbind events
-
- if (opts.onUnblock === null) {
- opts.onUnblock = $el.data('blockUI.onUnblock');
- $el.removeData('blockUI.onUnblock');
- }
-
- var els;
- if (full) // crazy selector to handle odd field errors in ie6/7
- els = $('body').children().filter('.blockUI').add('body > .blockUI');
- else
- els = $el.find('>.blockUI');
-
- // fix cursor issue
- if ( opts.cursorReset ) {
- if ( els.length > 1 )
- els[1].style.cursor = opts.cursorReset;
- if ( els.length > 2 )
- els[2].style.cursor = opts.cursorReset;
- }
-
- if (full)
- pageBlock = pageBlockEls = null;
-
- if (opts.fadeOut) {
- count = els.length;
- els.fadeOut(opts.fadeOut, function() {
- if ( --count === 0)
- reset(els,data,opts,el);
- });
- }
- else
- reset(els, data, opts, el);
- }
-
- // move blocking element back into the DOM where it started
- function reset(els,data,opts,el) {
- var $el = $(el);
- els.each(function(i,o) {
- // remove via DOM calls so we don't lose event handlers
- if (this.parentNode)
- this.parentNode.removeChild(this);
- });
-
- if (data && data.el) {
- data.el.style.display = data.display;
- data.el.style.position = data.position;
- if (data.parent)
- data.parent.appendChild(data.el);
- $el.removeData('blockUI.history');
- }
-
- if ($el.data('blockUI.static')) {
- $el.css('position', 'static'); // #22
- }
-
- if (typeof opts.onUnblock == 'function')
- opts.onUnblock(el,opts);
-
- // fix issue in Safari 6 where block artifacts remain until reflow
- var body = $(document.body), w = body.width(), cssW = body[0].style.width;
- body.width(w-1).width(w);
- body[0].style.width = cssW;
- }
-
- // bind/unbind the handler
- function bind(b, el, opts) {
- var full = el == window, $el = $(el);
-
- // don't bother unbinding if there is nothing to unbind
- if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
- return;
-
- $el.data('blockUI.isBlocked', b);
-
- // don't bind events when overlay is not in use or if bindEvents is false
- if (!full || !opts.bindEvents || (b && !opts.showOverlay))
- return;
-
- // bind anchors and inputs for mouse and key events
- var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove';
- if (b)
- $(document).bind(events, opts, handler);
- else
- $(document).unbind(events, handler);
-
- // former impl...
- // var $e = $('a,:input');
- // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
- }
-
- // event handler to suppress keyboard/mouse events when blocking
- function handler(e) {
- // allow tab navigation (conditionally)
- if (e.keyCode && e.keyCode == 9) {
- if (pageBlock && e.data.constrainTabKey) {
- var els = pageBlockEls;
- var fwd = !e.shiftKey && e.target === els[els.length-1];
- var back = e.shiftKey && e.target === els[0];
- if (fwd || back) {
- setTimeout(function(){focus(back);},10);
- return false;
- }
- }
- }
- var opts = e.data;
- var target = $(e.target);
- if (target.hasClass('blockOverlay') && opts.onOverlayClick)
- opts.onOverlayClick();
-
- // allow events within the message content
- if (target.parents('div.' + opts.blockMsgClass).length > 0)
- return true;
-
- // allow events for content that is not being blocked
- return target.parents().children().filter('div.blockUI').length === 0;
- }
-
- function focus(back) {
- if (!pageBlockEls)
- return;
- var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0];
- if (e)
- e.focus();
- }
-
- function center(el, x, y) {
- var p = el.parentNode, s = el.style;
- var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth');
- var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth');
- if (x) s.left = l > 0 ? (l+'px') : '0';
- if (y) s.top = t > 0 ? (t+'px') : '0';
- }
-
- function sz(el, p) {
- return parseInt($.css(el,p),10)||0;
- }
-
- }
-
-
- /*global define:true */
- if (typeof define === 'function' && define.amd && define.amd.jQuery) {
- define(['jquery'], setup);
- } else {
- setup(jQuery);
- }
-
-})();
diff --git a/vendor/assets/javascripts/jquery.history.js b/vendor/assets/javascripts/jquery.history.js
deleted file mode 100644
index 8d4edcd210e..00000000000
--- a/vendor/assets/javascripts/jquery.history.js
+++ /dev/null
@@ -1 +0,0 @@
-window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;c<f;c+=1)d=rep[c],typeof d=="string"&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));return e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var JSON=window.JSON,cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;d<c;d+=1)indent+=" ";else typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),typeof reviver=="function"?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),function(a,b){"use strict";var c=a.History=a.History||{},d=a.jQuery;if(typeof c.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");c.Adapter={bind:function(a,b,c){d(a).bind(b,c)},trigger:function(a,b,c){d(a).trigger(b,c)},extractEventData:function(a,c,d){var e=c&&c.originalEvent&&c.originalEvent[a]||d&&d[a]||b;return e},onDomLoad:function(a){d(a)}},typeof c.init!="undefined"&&c.init()}(window),function(a,b){"use strict";var c=a.document,d=a.setTimeout||d,e=a.clearTimeout||e,f=a.setInterval||f,g=a.History=a.History||{};if(typeof g.initHtml4!="undefined")throw new Error("History.js HTML4 Support has already been loaded...");g.initHtml4=function(){if(typeof g.initHtml4.initialized!="undefined")return!1;g.initHtml4.initialized=!0,g.enabled=!0,g.savedHashes=[],g.isLastHash=function(a){var b=g.getHashByIndex(),c;return c=a===b,c},g.saveHash=function(a){return g.isLastHash(a)?!1:(g.savedHashes.push(a),!0)},g.getHashByIndex=function(a){var b=null;return typeof a=="undefined"?b=g.savedHashes[g.savedHashes.length-1]:a<0?b=g.savedHashes[g.savedHashes.length+a]:b=g.savedHashes[a],b},g.discardedHashes={},g.discardedStates={},g.discardState=function(a,b,c){var d=g.getHashByState(a),e;return e={discardedState:a,backState:c,forwardState:b},g.discardedStates[d]=e,!0},g.discardHash=function(a,b,c){var d={discardedHash:a,backState:c,forwardState:b};return g.discardedHashes[a]=d,!0},g.discardedState=function(a){var b=g.getHashByState(a),c;return c=g.discardedStates[b]||!1,c},g.discardedHash=function(a){var b=g.discardedHashes[a]||!1;return b},g.recycleState=function(a){var b=g.getHashByState(a);return g.discardedState(a)&&delete g.discardedStates[b],!0},g.emulated.hashChange&&(g.hashChangeInit=function(){g.checkerFunction=null;var b="",d,e,h,i;return g.isInternetExplorer()?(d="historyjs-iframe",e=c.createElement("iframe"),e.setAttribute("id",d),e.style.display="none",c.body.appendChild(e),e.contentWindow.document.open(),e.contentWindow.document.close(),h="",i=!1,g.checkerFunction=function(){if(i)return!1;i=!0;var c=g.getHash()||"",d=g.unescapeHash(e.contentWindow.document.location.hash)||"";return c!==b?(b=c,d!==c&&(h=d=c,e.contentWindow.document.open(),e.contentWindow.document.close(),e.contentWindow.document.location.hash=g.escapeHash(c)),g.Adapter.trigger(a,"hashchange")):d!==h&&(h=d,g.setHash(d,!1)),i=!1,!0}):g.checkerFunction=function(){var c=g.getHash();return c!==b&&(b=c,g.Adapter.trigger(a,"hashchange")),!0},g.intervalList.push(f(g.checkerFunction,g.options.hashChangeInterval)),!0},g.Adapter.onDomLoad(g.hashChangeInit)),g.emulated.pushState&&(g.onHashChange=function(b){var d=b&&b.newURL||c.location.href,e=g.getHashByUrl(d),f=null,h=null,i=null,j;return g.isLastHash(e)?(g.busy(!1),!1):(g.doubleCheckComplete(),g.saveHash(e),e&&g.isTraditionalAnchor(e)?(g.Adapter.trigger(a,"anchorchange"),g.busy(!1),!1):(f=g.extractState(g.getFullUrl(e||c.location.href,!1),!0),g.isLastSavedState(f)?(g.busy(!1),!1):(h=g.getHashByState(f),j=g.discardedState(f),j?(g.getHashByIndex(-2)===g.getHashByState(j.forwardState)?g.back(!1):g.forward(!1),!1):(g.pushState(f.data,f.title,f.url,!1),!0))))},g.Adapter.bind(a,"hashchange",g.onHashChange),g.pushState=function(b,d,e,f){if(g.getHashByUrl(e))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(f!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.pushState,args:arguments,queue:f}),!1;g.busy(!0);var h=g.createStateObject(b,d,e),i=g.getHashByState(h),j=g.getState(!1),k=g.getHashByState(j),l=g.getHash();return g.storeState(h),g.expectedStateId=h.id,g.recycleState(h),g.setTitle(h),i===k?(g.busy(!1),!1):i!==l&&i!==g.getShortUrl(c.location.href)?(g.setHash(i,!1),!1):(g.saveState(h),g.Adapter.trigger(a,"statechange"),g.busy(!1),!0)},g.replaceState=function(a,b,c,d){if(g.getHashByUrl(c))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(d!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.replaceState,args:arguments,queue:d}),!1;g.busy(!0);var e=g.createStateObject(a,b,c),f=g.getState(!1),h=g.getStateByIndex(-2);return g.discardState(f,e,h),g.pushState(e.data,e.title,e.url,!1),!0}),g.emulated.pushState&&g.getHash()&&!g.emulated.hashChange&&g.Adapter.onDomLoad(function(){g.Adapter.trigger(a,"hashchange")})},typeof g.init!="undefined"&&g.init()}(window),function(a,b){"use strict";var c=a.console||b,d=a.document,e=a.navigator,f=a.sessionStorage||!1,g=a.setTimeout,h=a.clearTimeout,i=a.setInterval,j=a.clearInterval,k=a.JSON,l=a.alert,m=a.History=a.History||{},n=a.history;k.stringify=k.stringify||k.encode,k.parse=k.parse||k.decode;if(typeof m.init!="undefined")throw new Error("History.js Core has already been loaded...");m.init=function(){return typeof m.Adapter=="undefined"?!1:(typeof m.initCore!="undefined"&&m.initCore(),typeof m.initHtml4!="undefined"&&m.initHtml4(),!0)},m.initCore=function(){if(typeof m.initCore.initialized!="undefined")return!1;m.initCore.initialized=!0,m.options=m.options||{},m.options.hashChangeInterval=m.options.hashChangeInterval||100,m.options.safariPollInterval=m.options.safariPollInterval||500,m.options.doubleCheckInterval=m.options.doubleCheckInterval||500,m.options.storeInterval=m.options.storeInterval||1e3,m.options.busyDelay=m.options.busyDelay||250,m.options.debug=m.options.debug||!1,m.options.initialTitle=m.options.initialTitle||d.title,m.intervalList=[],m.clearAllIntervals=function(){var a,b=m.intervalList;if(typeof b!="undefined"&&b!==null){for(a=0;a<b.length;a++)j(b[a]);m.intervalList=null}},m.debug=function(){(m.options.debug||!1)&&m.log.apply(m,arguments)},m.log=function(){var a=typeof c!="undefined"&&typeof c.log!="undefined"&&typeof c.log.apply!="undefined",b=d.getElementById("log"),e,f,g,h,i;a?(h=Array.prototype.slice.call(arguments),e=h.shift(),typeof c.debug!="undefined"?c.debug.apply(c,[e,h]):c.log.apply(c,[e,h])):e="\n"+arguments[0]+"\n";for(f=1,g=arguments.length;f<g;++f){i=arguments[f];if(typeof i=="object"&&typeof k!="undefined")try{i=k.stringify(i)}catch(j){}e+="\n"+i+"\n"}return b?(b.value+=e+"\n-----\n",b.scrollTop=b.scrollHeight-b.clientHeight):a||l(e),!0},m.getInternetExplorerMajorVersion=function(){var a=m.getInternetExplorerMajorVersion.cached=typeof m.getInternetExplorerMajorVersion.cached!="undefined"?m.getInternetExplorerMajorVersion.cached:function(){var a=3,b=d.createElement("div"),c=b.getElementsByTagName("i");while((b.innerHTML="<!--[if gt IE "+ ++a+"]><i></i><![endif]-->")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","&lt;").replace(">","&gt;").replace(" & "," &amp; ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file