summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--CHANGELOG82
-rw-r--r--CONTRIBUTING.md4
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile19
-rw-r--r--Gemfile.lock111
-rw-r--r--LICENSE2
-rw-r--r--Procfile2
-rwxr-xr-xapp/assets/fonts/OFL.txt7
-rw-r--r--app/assets/fonts/SourceSansPro-Black.ttfbin289364 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Black.ttf.woffbin0 -> 113800 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BlackIt.ttfbin103404 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BlackIt.ttf.woffbin0 -> 49704 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BlackItalic.ttfbin116360 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Bold.ttfbin291424 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Bold.ttf.woffbin0 -> 117872 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-BoldIt.ttfbin103608 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BoldIt.ttf.woffbin0 -> 50608 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BoldItalic.ttfbin116192 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLight.ttfbin291652 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLight.ttf.woffbin0 -> 114336 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-ExtraLightIt.ttfbin104768 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woffbin0 -> 49684 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLightItalic.ttfbin117140 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-It.ttfbin104236 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-It.ttf.woffbin0 -> 51012 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Italic.ttfbin117328 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Light.ttfbin293220 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Light.ttf.woffbin0 -> 118284 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-LightIt.ttfbin104616 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-LightIt.ttf.woffbin0 -> 50992 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-LightItalic.ttfbin116960 -> 0 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Regular.ttfbin293956 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Regular.ttf.woffbin0 -> 119064 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-Semibold.ttfbin292404 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Semibold.ttf.woffbin0 -> 118412 bytes
-rw-r--r--app/assets/fonts/SourceSansPro-SemiboldIt.ttfbin104020 -> 0 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-SemiboldIt.ttf.woffbin0 -> 50924 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-SemiboldItalic.ttfbin116424 -> 0 bytes
-rw-r--r--app/assets/images/auth_buttons/azure_64.pngbin0 -> 986 bytes
-rw-r--r--app/assets/javascripts/activities.js.coffee4
-rw-r--r--app/assets/javascripts/admin.js.coffee10
-rw-r--r--app/assets/javascripts/application.js.coffee7
-rw-r--r--app/assets/javascripts/behaviors/autosize.js.coffee4
-rw-r--r--app/assets/javascripts/branch-graph.js.coffee2
-rw-r--r--app/assets/javascripts/calendar.js.coffee5
-rw-r--r--app/assets/javascripts/commits.js.coffee66
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee4
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee14
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee8
-rw-r--r--app/assets/javascripts/issue.js.coffee12
-rw-r--r--app/assets/javascripts/issues.js.coffee11
-rw-r--r--app/assets/javascripts/logo.js.coffee44
-rw-r--r--app/assets/javascripts/merge_request.js.coffee23
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee2
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee4
-rw-r--r--app/assets/javascripts/notes.js.coffee20
-rw-r--r--app/assets/javascripts/project_find_file.js.coffee125
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js.coffee19
-rw-r--r--app/assets/javascripts/shortcuts_tree.coffee4
-rw-r--r--app/assets/javascripts/users_select.js.coffee2
-rw-r--r--app/assets/javascripts/wikis.js.coffee25
-rw-r--r--app/assets/javascripts/zen_mode.js.coffee104
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/blocks.scss34
-rw-r--r--app/assets/stylesheets/framework/buttons.scss80
-rw-r--r--app/assets/stylesheets/framework/calendar.scss42
-rw-r--r--app/assets/stylesheets/framework/common.scss69
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/flash.scss2
-rw-r--r--app/assets/stylesheets/framework/fonts.scss8
-rw-r--r--app/assets/stylesheets/framework/forms.scss6
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss13
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss25
-rw-r--r--app/assets/stylesheets/framework/mixins.scss35
-rw-r--r--app/assets/stylesheets/framework/mobile.scss9
-rw-r--r--app/assets/stylesheets/framework/nav.scss39
-rw-r--r--app/assets/stylesheets/framework/selects.scss4
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss5
-rw-r--r--app/assets/stylesheets/framework/tables.scss7
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap.scss41
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss8
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/framework/zen.scss101
-rw-r--r--app/assets/stylesheets/pages/branches.scss3
-rw-r--r--app/assets/stylesheets/pages/commit.scss6
-rw-r--r--app/assets/stylesheets/pages/commits.scss60
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss14
-rw-r--r--app/assets/stylesheets/pages/events.scss5
-rw-r--r--app/assets/stylesheets/pages/groups.scss5
-rw-r--r--app/assets/stylesheets/pages/issuable.scss19
-rw-r--r--app/assets/stylesheets/pages/issues.scss7
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/note_form.scss1
-rw-r--r--app/assets/stylesheets/pages/projects.scss238
-rw-r--r--app/assets/stylesheets/pages/tags.scss3
-rw-r--r--app/assets/stylesheets/pages/tree.scss26
-rw-r--r--app/controllers/abuse_reports_controller.rb11
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb6
-rw-r--r--app/controllers/admin/application_settings_controller.rb8
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb33
-rw-r--r--app/controllers/admin/builds_controller.rb6
-rw-r--r--app/controllers/admin/identities_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb4
-rw-r--r--app/controllers/application_controller.rb2
-rw-r--r--app/controllers/ci/lints_controller.rb6
-rw-r--r--app/controllers/explore/groups_controller.rb2
-rw-r--r--app/controllers/projects/artifacts_controller.rb56
-rw-r--r--app/controllers/projects/branches_controller.rb5
-rw-r--r--app/controllers/projects/builds_controller.rb33
-rw-r--r--app/controllers/projects/commits_controller.rb10
-rw-r--r--app/controllers/projects/find_file_controller.rb26
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/refs_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/sent_notifications_controller.rb25
-rw-r--r--app/controllers/sessions_controller.rb5
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/groups_finder.rb44
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/joined_groups_finder.rb49
-rw-r--r--app/helpers/application_helper.rb12
-rw-r--r--app/helpers/auth_helper.rb2
-rw-r--r--app/helpers/broadcast_messages_helper.rb30
-rw-r--r--app/helpers/button_helper.rb2
-rw-r--r--app/helpers/events_helper.rb8
-rw-r--r--app/helpers/gitlab_markdown_helper.rb2
-rw-r--r--app/helpers/issues_helper.rb13
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/helpers/page_layout_helper.rb25
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb8
-rw-r--r--app/mailers/abuse_report_mailer.rb10
-rw-r--r--app/mailers/emails/issues.rb38
-rw-r--r--app/mailers/emails/merge_requests.rb55
-rw-r--r--app/mailers/emails/notes.rb44
-rw-r--r--app/mailers/notify.rb5
-rw-r--r--app/models/ability.rb4
-rw-r--r--app/models/abuse_report.rb11
-rw-r--r--app/models/application_setting.rb43
-rw-r--r--app/models/broadcast_message.rb18
-rw-r--r--app/models/ci/build.rb108
-rw-r--r--app/models/ci/runner_project.rb11
-rw-r--r--app/models/ci/trigger.rb17
-rw-r--r--app/models/ci/variable.rb9
-rw-r--r--app/models/commit_status.rb63
-rw-r--r--app/models/concerns/issuable.rb12
-rw-r--r--app/models/concerns/mentionable.rb7
-rw-r--r--app/models/concerns/sortable.rb3
-rw-r--r--app/models/generic_commit_status.rb1
-rw-r--r--app/models/global_milestone.rb4
-rw-r--r--app/models/group.rb9
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/service_hook.rb1
-rw-r--r--app/models/hooks/system_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb7
-rw-r--r--app/models/identity.rb4
-rw-r--r--app/models/issue.rb8
-rw-r--r--app/models/merge_request.rb62
-rw-r--r--app/models/milestone.rb26
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/note.rb10
-rw-r--r--app/models/project.rb32
-rw-r--r--app/models/project_services/asana_service.rb84
-rw-r--r--app/models/project_services/assembla_service.rb1
-rw-r--r--app/models/project_services/bamboo_service.rb1
-rw-r--r--app/models/project_services/buildkite_service.rb1
-rw-r--r--app/models/project_services/builds_email_service.rb7
-rw-r--r--app/models/project_services/campfire_service.rb1
-rw-r--r--app/models/project_services/ci_service.rb1
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb1
-rw-r--r--app/models/project_services/drone_ci_service.rb1
-rw-r--r--app/models/project_services/emails_on_push_service.rb1
-rw-r--r--app/models/project_services/external_wiki_service.rb1
-rw-r--r--app/models/project_services/flowdock_service.rb1
-rw-r--r--app/models/project_services/gemnasium_service.rb1
-rw-r--r--app/models/project_services/gitlab_ci_service.rb1
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb1
-rw-r--r--app/models/project_services/hipchat_service.rb9
-rw-r--r--app/models/project_services/irker_service.rb8
-rw-r--r--app/models/project_services/issue_tracker_service.rb1
-rw-r--r--app/models/project_services/jira_service.rb11
-rw-r--r--app/models/project_services/pivotaltracker_service.rb1
-rw-r--r--app/models/project_services/pushover_service.rb1
-rw-r--r--app/models/project_services/redmine_service.rb1
-rw-r--r--app/models/project_services/slack_service.rb1
-rw-r--r--app/models/project_services/teamcity_service.rb1
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/repository.rb49
-rw-r--r--app/models/sent_notification.rb12
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/tree.rb14
-rw-r--r--app/models/user.rb132
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/services/create_branch_service.rb1
-rw-r--r--app/services/create_tag_service.rb1
-rw-r--r--app/services/delete_branch_service.rb1
-rw-r--r--app/services/merge_requests/merge_service.rb2
-rw-r--r--app/services/notification_service.rb1
-rw-r--r--app/services/projects/download_service.rb8
-rw-r--r--app/services/projects/housekeeping_service.rb20
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/upload_service.rb8
-rw-r--r--app/services/repair_ldap_blocked_user_service.rb17
-rw-r--r--app/services/system_hooks_service.rb17
-rw-r--r--app/services/system_note_service.rb6
-rw-r--r--app/uploaders/artifact_uploader.rb8
-rw-r--r--app/uploaders/file_uploader.rb15
-rw-r--r--app/validators/namespace_validator.rb1
-rw-r--r--app/views/abuse_report_mailer/notify.html.haml2
-rw-r--r--app/views/abuse_reports/new.html.haml4
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml23
-rw-r--r--app/views/admin/abuse_reports/index.html.haml3
-rw-r--r--app/views/admin/application_settings/_form.html.haml49
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml37
-rw-r--r--app/views/admin/broadcast_messages/edit.html.haml3
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml79
-rw-r--r--app/views/admin/builds/_build.html.haml4
-rw-r--r--app/views/admin/builds/index.html.haml16
-rw-r--r--app/views/admin/dashboard/index.html.haml22
-rw-r--r--app/views/admin/groups/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml2
-rw-r--r--app/views/admin/labels/index.html.haml2
-rw-r--r--app/views/admin/logs/show.html.haml5
-rw-r--r--app/views/admin/projects/show.html.haml4
-rw-r--r--app/views/admin/users/_head.html.haml7
-rw-r--r--app/views/admin/users/_profile.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml190
-rw-r--r--app/views/admin/users/show.html.haml24
-rw-r--r--app/views/ci/lints/show.html.haml6
-rw-r--r--app/views/dashboard/_activities.html.haml4
-rw-r--r--app/views/dashboard/_activity_head.html.haml2
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/dashboard/_snippets_head.html.haml2
-rw-r--r--app/views/dashboard/issues.atom.builder2
-rw-r--r--app/views/dashboard/milestones/show.html.haml4
-rw-r--r--app/views/dashboard/projects/index.atom.builder2
-rw-r--r--app/views/dashboard/snippets/index.html.haml48
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/_event_last_push.html.haml2
-rw-r--r--app/views/groups/edit.html.haml11
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/issues.atom.builder2
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/show.atom.builder2
-rw-r--r--app/views/groups/show.html.haml41
-rw-r--r--app/views/help/_shortcuts.html.haml26
-rw-r--r--app/views/help/ui.html.haml21
-rw-r--r--app/views/layouts/_broadcast.html.haml5
-rw-r--r--app/views/layouts/_head.html.haml14
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml8
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/group.html.haml7
-rw-r--r--app/views/layouts/nav/_admin.html.haml6
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/layouts/nav/_group.html.haml4
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml9
-rw-r--r--app/views/layouts/notify.html.haml8
-rw-r--r--app/views/layouts/project.html.haml7
-rw-r--r--app/views/notify/repository_push_email.html.haml2
-rw-r--r--app/views/notify/repository_push_email.text.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml2
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/projects/_activity.html.haml4
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_find_file_link.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml38
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_zen.html.haml11
-rw-r--r--app/views/projects/artifacts/_tree_directory.html.haml7
-rw-r--r--app/views/projects/artifacts/_tree_file.html.haml11
-rw-r--r--app/views/projects/artifacts/browse.html.haml24
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml17
-rw-r--r--app/views/projects/builds/index.html.haml16
-rw-r--r--app/views/projects/builds/show.html.haml14
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml9
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml2
-rw-r--r--app/views/projects/commit/_commit_box.html.haml2
-rw-r--r--app/views/projects/commit/builds.html.haml3
-rw-r--r--app/views/projects/commit/show.html.haml8
-rw-r--r--app/views/projects/commit_statuses/_commit_status.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/commits/_commits.html.haml2
-rw-r--r--app/views/projects/commits/_head.html.haml2
-rw-r--r--app/views/projects/commits/show.atom.builder4
-rw-r--r--app/views/projects/commits/show.html.haml28
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/edit.html.haml15
-rw-r--r--app/views/projects/empty.html.haml71
-rw-r--r--app/views/projects/find_file/show.html.haml27
-rw-r--r--app/views/projects/graphs/_head.html.haml2
-rw-r--r--app/views/projects/hooks/index.html.haml4
-rw-r--r--app/views/projects/issues/_closed_by_box.html.haml6
-rw-r--r--app/views/projects/issues/_discussion.html.haml6
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml6
-rw-r--r--app/views/projects/issues/index.atom.builder2
-rw-r--r--app/views/projects/issues/show.html.haml3
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml4
-rw-r--r--app/views/projects/merge_requests/index.html.haml5
-rw-r--r--app/views/projects/milestones/_milestone.html.haml7
-rw-r--r--app/views/projects/milestones/show.html.haml14
-rw-r--r--app/views/projects/notes/_edit_form.html.haml4
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/notes/_notes.html.haml4
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/runners/index.html.haml3
-rw-r--r--app/views/projects/show.atom.builder2
-rw-r--r--app/views/projects/show.html.haml34
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml4
-rw-r--r--app/views/projects/tree/_tree_content.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml10
-rw-r--r--app/views/projects/wikis/_nav.html.haml2
-rw-r--r--app/views/projects/wikis/_new.html.haml11
-rw-r--r--app/views/projects/wikis/git_access.html.haml12
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_clone_panel.html.haml4
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_logo.svg14
-rw-r--r--app/views/shared/_milestones_filter.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml4
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--app/views/shared/issuable/_filter.html.haml24
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml20
-rw-r--r--app/views/shared/projects/_project.html.haml13
-rw-r--r--app/views/sherlock/queries/show.html.haml2
-rw-r--r--app/views/sherlock/transactions/show.html.haml2
-rw-r--r--app/views/users/show.atom.builder2
-rw-r--r--app/views/users/show.html.haml128
-rw-r--r--app/views/votes/_votes_block.html.haml30
-rw-r--r--app/workers/metrics_worker.rb33
-rw-r--r--config/gitlab.yml.example11
-rw-r--r--config/initializers/1_settings.rb15
-rw-r--r--config/initializers/date_time_formats.rb9
-rw-r--r--config/initializers/metrics.rb1
-rw-r--r--config/initializers/recaptcha.rb6
-rw-r--r--config/routes.rb42
-rw-r--r--config/schedule.rb8
-rw-r--r--db/fixtures/development/14_builds.rb79
-rw-r--r--db/migrate/20151228111122_remove_public_from_namespace.rb6
-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/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb5
-rw-r--r--db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb5
-rw-r--r--db/migrate/20160106162223_add_index_milestones_title.rb5
-rw-r--r--db/migrate/20160106164438_remove_influxdb_credentials.rb6
-rw-r--r--db/migrate/20160113111034_add_metrics_sample_interval.rb6
-rw-r--r--db/schema.rb99
-rw-r--r--doc/README.md7
-rw-r--r--doc/administration/environment_variables.md (renamed from doc/administration/enviroment_variables.md)14
-rw-r--r--doc/api/README.md3
-rw-r--r--doc/api/build_triggers.md108
-rw-r--r--doc/api/build_variables.md128
-rw-r--r--doc/api/builds.md360
-rw-r--r--doc/api/merge_requests.md17
-rw-r--r--doc/api/notes.md3
-rw-r--r--doc/api/projects.md45
-rw-r--r--doc/api/tags.md20
-rw-r--r--doc/api/users.md13
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/enable_or_disable_ci.md70
-rw-r--r--doc/ci/img/features_settings.pngbin0 -> 18691 bytes
-rw-r--r--doc/ci/runners/README.md5
-rw-r--r--doc/development/doc_styleguide.md231
-rw-r--r--doc/incoming_email/README.md3
-rw-r--r--doc/incoming_email/postfix.md15
-rw-r--r--doc/install/installation.md18
-rw-r--r--doc/integration/README.md29
-rw-r--r--doc/integration/azure.md83
-rw-r--r--doc/integration/external-issue-tracker.md48
-rw-r--r--doc/integration/img/jira_issue_reference.png (renamed from doc/integration/jira_issue_reference.png)bin39942 -> 39942 bytes
-rw-r--r--doc/integration/img/jira_merge_request_close.pngbin0 -> 111150 bytes
-rw-r--r--doc/integration/img/jira_project_name.png (renamed from doc/integration/jira_project_name.png)bin60598 -> 60598 bytes
-rw-r--r--doc/integration/img/jira_service.png (renamed from doc/integration/jira_service.png)bin59082 -> 59082 bytes
-rw-r--r--doc/integration/img/jira_service_close_issue.png (renamed from doc/integration/jira_service_close_issue.png)bin88433 -> 88433 bytes
-rw-r--r--doc/integration/img/jira_service_page.pngbin0 -> 35496 bytes
-rw-r--r--doc/integration/img/jira_workflow_screenshot.png (renamed from doc/integration/jira_workflow_screenshot.png)bin121534 -> 121534 bytes
-rw-r--r--doc/integration/jira.md138
-rw-r--r--doc/integration/jira_service_page.pngbin162449 -> 0 bytes
-rw-r--r--doc/integration/ldap.md5
-rw-r--r--doc/integration/omniauth.md1
-rw-r--r--doc/integration/recaptcha.md47
-rw-r--r--doc/integration/redmine_configuration.pngbin118752 -> 0 bytes
-rw-r--r--doc/integration/redmine_service_template.pngbin198077 -> 0 bytes
-rw-r--r--doc/integration/shibboleth.md4
-rw-r--r--doc/project_services/img/redmine_configuration.pngbin0 -> 21061 bytes
-rw-r--r--doc/project_services/img/services_templates_redmine_example.pngbin0 -> 17351 bytes
-rw-r--r--doc/project_services/project_services.md49
-rw-r--r--doc/project_services/redmine.md21
-rw-r--r--doc/project_services/services_templates.md25
-rw-r--r--doc/raketasks/backup_restore.md43
-rw-r--r--doc/release/patch.md2
-rw-r--r--doc/ssh/README.md11
-rw-r--r--doc/system_hooks/system_hooks.md56
-rw-r--r--doc/update/8.2-to-8.3.md2
-rw-r--r--doc/update/8.3-to-8.4.md148
-rw-r--r--doc/update/patch_versions.md1
-rw-r--r--doc/workflow/add-user/add-user.md90
-rw-r--r--doc/workflow/add-user/images/add-members.pngbin2361 -> 0 bytes
-rw-r--r--doc/workflow/add-user/images/new-member.pngbin12038 -> 0 bytes
-rw-r--r--doc/workflow/add-user/images/select-project.pngbin4042 -> 0 bytes
-rw-r--r--doc/workflow/add-user/img/add_new_user_to_project_settings.pngbin0 -> 22822 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_accept.pngbin0 -> 10833 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_ready.pngbin0 -> 16177 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_search.pngbin0 -> 15889 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_give_permissions.pngbin0 -> 22089 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_import_members_from_another_project.pngbin0 -> 18897 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_imported_members.pngbin0 -> 23897 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_list_members.pngbin0 -> 15732 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_members_menu.png (renamed from doc/workflow/add-user/images/members.png)bin8295 -> 8295 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_search_people.pngbin0 -> 13518 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_github.md4
-rw-r--r--doc/workflow/shortcuts.pngbin78736 -> 48782 bytes
-rw-r--r--doc_styleguide.md25
-rw-r--r--features/admin/broadcast_messages.feature20
-rw-r--r--features/explore/groups.feature15
-rw-r--r--features/project/builds.feature58
-rw-r--r--features/project/commits/commits.feature5
-rw-r--r--features/project/find_file.feature42
-rw-r--r--features/project/fork.feature11
-rw-r--r--features/project/issues/references.feature33
-rw-r--r--features/project/merge_requests/references.feature31
-rw-r--r--features/project/wiki.feature5
-rw-r--r--features/steps/admin/broadcast_messages.rb37
-rw-r--r--features/steps/group/milestones.rb2
-rw-r--r--features/steps/project/builds.rb89
-rw-r--r--features/steps/project/commits/commits.rb9
-rw-r--r--features/steps/project/fork.rb19
-rw-r--r--features/steps/project/issues/award_emoji.rb2
-rw-r--r--features/steps/project/issues/issues.rb9
-rw-r--r--features/steps/project/issues/references.rb7
-rw-r--r--features/steps/project/merge_requests/references.rb7
-rw-r--r--features/steps/project/project_find_file.rb73
-rw-r--r--features/steps/project/wiki.rb10
-rw-r--r--features/steps/shared/active_tab.rb4
-rw-r--r--features/steps/shared/builds.rb28
-rw-r--r--features/steps/shared/issuable.rb134
-rw-r--r--features/steps/shared/note.rb4
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--features/steps/shared/project.rb41
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/builds.rb149
-rw-r--r--lib/api/entities.rb37
-rw-r--r--lib/api/helpers.rb34
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/notes.rb21
-rw-r--r--lib/api/projects.rb31
-rw-r--r--lib/api/tags.rb21
-rw-r--r--lib/api/triggers.rb69
-rw-r--r--lib/api/users.rb16
-rw-r--r--lib/api/variables.rb95
-rw-r--r--lib/banzai/cross_project_reference.rb2
-rw-r--r--lib/banzai/filter.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb52
-rw-r--r--lib/banzai/filter/autolink_filter.rb1
-rw-r--r--lib/banzai/filter/commit_range_reference_filter.rb2
-rw-r--r--lib/banzai/filter/commit_reference_filter.rb2
-rw-r--r--lib/banzai/filter/emoji_filter.rb1
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/external_link_filter.rb1
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb151
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/label_reference_filter.rb2
-rw-r--r--lib/banzai/filter/markdown_filter.rb1
-rw-r--r--lib/banzai/filter/merge_request_reference_filter.rb2
-rw-r--r--lib/banzai/filter/milestone_reference_filter.rb22
-rw-r--r--lib/banzai/filter/redactor_filter.rb3
-rw-r--r--lib/banzai/filter/reference_filter.rb9
-rw-r--r--lib/banzai/filter/reference_gatherer_filter.rb3
-rw-r--r--lib/banzai/filter/relative_link_filter.rb3
-rw-r--r--lib/banzai/filter/sanitization_filter.rb1
-rw-r--r--lib/banzai/filter/snippet_reference_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb1
-rw-r--r--lib/banzai/filter/table_of_contents_filter.rb1
-rw-r--r--lib/banzai/filter/task_list_filter.rb14
-rw-r--r--lib/banzai/filter/upload_link_filter.rb1
-rw-r--r--lib/banzai/filter/user_reference_filter.rb2
-rw-r--r--lib/banzai/lazy_reference.rb2
-rw-r--r--lib/banzai/pipeline.rb2
-rw-r--r--lib/banzai/pipeline/asciidoc_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/atom_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/base_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/combined_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/description_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/email_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/full_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb3
-rw-r--r--lib/banzai/pipeline/note_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/plain_markdown_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/reference_extraction_pipeline.rb2
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb18
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb11
-rw-r--r--lib/banzai/querying.rb18
-rw-r--r--lib/banzai/reference_extractor.rb2
-rw-r--r--lib/banzai/renderer.rb4
-rw-r--r--lib/ci/api/builds.rb22
-rw-r--r--lib/gitlab/backend/shell.rb12
-rw-r--r--lib/gitlab/build_data_builder.rb1
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata.rb109
-rw-r--r--lib/gitlab/ci/build/artifacts/metadata/entry.rb119
-rw-r--r--lib/gitlab/contributions_calendar.rb4
-rw-r--r--lib/gitlab/current_settings.rb7
-rw-r--r--lib/gitlab/diff/parser.rb5
-rw-r--r--lib/gitlab/email/receiver.rb7
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb4
-rw-r--r--lib/gitlab/github_import/base_formatter.rb21
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb45
-rw-r--r--lib/gitlab/github_import/importer.rb83
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb66
-rw-r--r--lib/gitlab/github_import/project_creator.rb3
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb101
-rw-r--r--lib/gitlab/github_import/wiki_formatter.rb19
-rw-r--r--lib/gitlab/gitlab_import/importer.rb2
-rw-r--r--lib/gitlab/ldap/access.rb14
-rw-r--r--lib/gitlab/ldap/adapter.rb24
-rw-r--r--lib/gitlab/ldap/config.rb4
-rw-r--r--lib/gitlab/markdown/pipeline.rb2
-rw-r--r--lib/gitlab/metrics.rb87
-rw-r--r--lib/gitlab/metrics/instrumentation.rb2
-rw-r--r--lib/gitlab/metrics/metric.rb12
-rw-r--r--lib/gitlab/metrics/obfuscated_sql.rb47
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb10
-rw-r--r--lib/gitlab/metrics/sampler.rb51
-rw-r--r--lib/gitlab/metrics/sidekiq_middleware.rb14
-rw-r--r--lib/gitlab/metrics/subscribers/action_view.rb11
-rw-r--r--lib/gitlab/metrics/subscribers/active_record.rb30
-rw-r--r--lib/gitlab/metrics/transaction.rb61
-rw-r--r--lib/gitlab/recaptcha.rb14
-rw-r--r--lib/gitlab/reference_extractor.rb4
-rw-r--r--lib/tasks/gitlab/check.rake2
-rw-r--r--lib/tasks/gitlab/task_helpers.rake2
-rw-r--r--lib/version_check.rb2
-rw-r--r--spec/controllers/abuse_reports_controller_spec.rb80
-rw-r--r--spec/controllers/admin/identities_controller_spec.rb26
-rw-r--r--spec/controllers/admin/users_controller_spec.rb35
-rw-r--r--spec/controllers/projects/find_file_controller_spec.rb66
-rw-r--r--spec/controllers/sent_notification_controller_spec.rb26
-rw-r--r--spec/factories.rb7
-rw-r--r--spec/factories/broadcast_messages.rb18
-rw-r--r--spec/factories/ci/builds.rb11
-rw-r--r--spec/factories/ci/trigger_requests.rb2
-rw-r--r--spec/factories/ci/variables.rb22
-rw-r--r--spec/factories/merge_requests.rb41
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/features/admin/admin_builds_spec.rb119
-rw-r--r--spec/features/builds_spec.rb31
-rw-r--r--spec/features/ci_lint_spec.rb8
-rw-r--r--spec/features/issues_spec.rb12
-rw-r--r--spec/features/markdown_spec.rb64
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/finders/groups_finder_spec.rb48
-rw-r--r--spec/finders/joined_groups_finder_spec.rb49
-rw-r--r--spec/fixtures/ci_build_artifacts.zipbin0 -> 106365 bytes
-rw-r--r--spec/fixtures/ci_build_artifacts_metadata.gzbin0 -> 415 bytes
-rw-r--r--spec/fixtures/markdown.md.erb16
-rw-r--r--spec/helpers/application_helper_spec.rb12
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb62
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb5
-rw-r--r--spec/helpers/page_layout_helper_spec.rb64
-rw-r--r--spec/helpers/search_helper_spec.rb2
-rw-r--r--spec/javascripts/fixtures/zen_mode.html.haml9
-rw-r--r--spec/javascripts/issue_spec.js.coffee34
-rw-r--r--spec/javascripts/zen_mode_spec.js.coffee26
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb89
-rw-r--r--spec/lib/banzai/filter/milestone_reference_filter_spec.rb75
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb6
-rw-r--r--spec/lib/banzai/querying_spec.rb13
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb6
-rw-r--r--spec/lib/gitlab/build_data_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb168
-rw-r--r--spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb84
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb11
-rw-r--r--spec/lib/gitlab/github_import/comment_formatter_spec.rb80
-rw-r--r--spec/lib/gitlab/github_import/issue_formatter_spec.rb139
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb184
-rw-r--r--spec/lib/gitlab/github_import/wiki_formatter_spec.rb22
-rw-r--r--spec/lib/gitlab/ldap/access_spec.rb35
-rw-r--r--spec/lib/gitlab/metrics/instrumentation_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/metric_spec.rb6
-rw-r--r--spec/lib/gitlab/metrics/obfuscated_sql_spec.rb87
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/sampler_spec.rb62
-rw-r--r--spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/subscribers/action_view_spec.rb12
-rw-r--r--spec/lib/gitlab/metrics/subscribers/active_record_spec.rb33
-rw-r--r--spec/lib/gitlab/metrics/transaction_spec.rb69
-rw-r--r--spec/lib/gitlab/metrics_spec.rb61
-rw-r--r--spec/mailers/abuse_report_mailer_spec.rb38
-rw-r--r--spec/mailers/notify_spec.rb34
-rw-r--r--spec/models/abuse_report_spec.rb33
-rw-r--r--spec/models/application_setting_spec.rb65
-rw-r--r--spec/models/broadcast_message_spec.rb72
-rw-r--r--spec/models/build_spec.rb85
-rw-r--r--spec/models/ci/build_spec.rb22
-rw-r--r--spec/models/ci/commit_spec.rb2
-rw-r--r--spec/models/ci/runner_project_spec.rb11
-rw-r--r--spec/models/ci/trigger_spec.rb13
-rw-r--r--spec/models/ci/variable_spec.rb3
-rw-r--r--spec/models/commit_status_spec.rb1
-rw-r--r--spec/models/concerns/issuable_spec.rb14
-rw-r--r--spec/models/external_wiki_service_spec.rb1
-rw-r--r--spec/models/generic_commit_status_spec.rb1
-rw-r--r--spec/models/group_spec.rb28
-rw-r--r--spec/models/hooks/web_hook_spec.rb12
-rw-r--r--spec/models/identity_spec.rb38
-rw-r--r--spec/models/merge_request_spec.rb41
-rw-r--r--spec/models/namespace_spec.rb1
-rw-r--r--spec/models/note_spec.rb39
-rw-r--r--spec/models/project_services/asana_service_spec.rb77
-rw-r--r--spec/models/project_services/builds_email_service_spec.rb23
-rw-r--r--spec/models/project_spec.rb7
-rw-r--r--spec/models/project_wiki_spec.rb7
-rw-r--r--spec/models/service_spec.rb1
-rw-r--r--spec/models/user_spec.rb159
-rw-r--r--spec/requests/api/builds_spec.rb172
-rw-r--r--spec/requests/api/commit_status_spec.rb6
-rw-r--r--spec/requests/api/notes_spec.rb56
-rw-r--r--spec/requests/api/projects_spec.rb23
-rw-r--r--spec/requests/api/tags_spec.rb21
-rw-r--r--spec/requests/api/triggers_spec.rb139
-rw-r--r--spec/requests/api/users_spec.rb23
-rw-r--r--spec/requests/api/variables_spec.rb182
-rw-r--r--spec/requests/ci/api/builds_spec.rb46
-rw-r--r--spec/routing/project_routing_spec.rb13
-rw-r--r--spec/services/notification_service_spec.rb15
-rw-r--r--spec/services/projects/download_service_spec.rb24
-rw-r--r--spec/services/repair_ldap_blocked_user_service_spec.rb23
-rw-r--r--spec/services/system_hooks_service_spec.rb47
-rw-r--r--spec/services/system_note_service_spec.rb2
-rw-r--r--spec/support/api/pagination_shared_examples.rb20
-rw-r--r--spec/support/markdown_feature.rb12
-rw-r--r--spec/support/matchers/markdown_matchers.rb27
-rw-r--r--spec/workers/metrics_worker_spec.rb52
-rwxr-xr-xvendor/assets/javascripts/autosize.js243
-rw-r--r--vendor/assets/javascripts/fuzzaldrin-plus.min.js1
-rw-r--r--vendor/assets/javascripts/jquery.blockUI.js590
-rw-r--r--vendor/assets/javascripts/jquery.history.js1
-rw-r--r--vendor/assets/javascripts/latinise.js11
665 files changed, 10115 insertions, 4235 deletions
diff --git a/.gitignore b/.gitignore
index f5b6427ca03..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
diff --git a/CHANGELOG b/CHANGELOG
index 57f0b9f30d5..7bbc0fc79f0 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,28 +1,91 @@
Please view this file on the master branch, on stable branches it's out of date.
+v 8.5.0 (unreleased)
+ - Remove gray background from layout in UI
+
v 8.4.0 (unreleased)
- - Add support for Google reCAPTCHA in user registration to prevent spammers (Stan Hu)
+ - Add pagination headers to already paginated API resources
+ - Properly generate diff of orphan commits, like the first commit in a repository
+ - Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages
+ - Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse)
+ - Improved performance of finding issues for an entire group (Yorick Peterse)
+ - Added custom application performance measuring system powered by InfluxDB (Yorick Peterse)
+ - Bump fog to 1.36.0 (Stan Hu)
+ - Add user's last used IP addresses to admin page (Stan Hu)
+ - Add housekeeping function to project settings page
+ - The default GitLab logo now acts as a loading indicator
+ - Fix caching issue where build status was not updating in project dashboard (Stan Hu)
+ - Accept 2xx status codes for successful Web hook triggers (Stan Hu)
+ - Fix missing date of month in network graph when commits span a month (Stan Hu)
+ - Expire view caches when application settings change (e.g. Gravatar disabled) (Stan Hu)
+ - Don't notify users twice if they are both project watchers and subscribers (Stan Hu)
- 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)
+ - Link to milestone in "Milestone changed" system note
- Only allow group/project members to mention `@all`
- - Expose Git's version in the admin area
+ - 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.
+ - Add link to merge request on build detail page
+ - Fix: Problem with projects ending with .keys (Jose Corcuera)
- Revert back upvote and downvote button to the issue and MR pages
-
-v 8.3.2 (unreleased)
+ - 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
+ - Add Open Graph and Twitter Card data to all pages
+ - Fix API project lookups when querying with a namespace with dots (Stan Hu)
+ - Enable forcing Two-Factor authentication sitewide, with optional grace period
+ - Import GitHub Pull Requests into GitLab
+ - Change single user API endpoint to return more detailed data (Michael Potthoff)
+ - Update version check images to use SVG
+ - Validate README format before displaying
+ - Enable Microsoft Azure OAuth2 support (Janis Meybohm)
+ - Properly set task-list class on single item task lists
+ - Add file finder feature in tree view (Kyungchul Shin)
+ - Ajax filter by message for commits page
+ - API: Add support for deleting a tag via the API (Robert Schilling)
+ - Allow subsequent validations in CI Linter
+ - Show referenced MRs & Issues only when the current viewer can access them
+ - Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
+ - Add API support for managing project's builds
+ - Add API support for managing project's build triggers
+ - Add API support for managing project's build variables
+ - Allow broadcast messages to be edited
+ - Autosize Markdown textareas
+ - Import GitHub wiki into GitLab
+
+v 8.3.4
+ - Use gitlab-workhorse 0.5.4 (fixes API routing bug)
+ - Add build artifacts browser
+
+v 8.3.3
+ - Preserve CE behavior with JIRA integration by only calling API if URL is set
+ - Fix duplicated branch creation/deletion events when using Web UI (Stan Hu)
+ - Add configurable LDAP server query timeout
+ - Get "Merge when build succeeds" to work when commits were pushed to MR target branch while builds were running
+ - Suppress e-mails on failed builds if allow_failure is set (Stan Hu)
+ - Fix project transfer e-mail sending incorrect paths in e-mail notification (Stan Hu)
+ - Better support for referencing and closing issues in Asana service (Mike Wyatt)
+ - Enable "Add key" button when user fills in a proper key (Stan Hu)
+ - Fix error in processing reply-by-email messages (Jason Lee)
+ - Fix Error 500 when visiting build page of project with nil runners_token (Stan Hu)
+ - Use WOFF versions of SourceSansPro fonts
+ - Fix regression when builds were not generated for tags created through web/api interface
+ - Fix: maintain milestone filter between Open and Closed tabs (Greg Smethells)
+ - Fix missing artifacts and build traces for build created before 8.3
+
+v 8.3.2
- Disable --follow in `git log` to avoid loading duplicate commit data in infinite scroll (Stan Hu)
- - Enable "Add key" button when user fills in a proper key
+ - 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
v 8.3.0
- Bump rack-attack to 4.3.1 for security fix (Stan Hu)
@@ -30,6 +93,7 @@ v 8.3.0
- Add open_issues_count to project API (Stan Hu)
- Expand character set of usernames created by Omniauth (Corey Hinshaw)
- Add button to automatically merge a merge request when the build succeeds (Zeger-Jan van de Weg)
+ - Add unsubscribe link in the email footer (Zeger-Jan van de Weg)
- Provide better diagnostic message upon project creation errors (Stan Hu)
- Bump devise to 3.5.3 to fix reset token expiring after account creation (Stan Hu)
- Remove api credentials from link to build_page
@@ -86,6 +150,8 @@ v 8.3.0
- Do not show build status unless builds are enabled and `.gitlab-ci.yml` is present
- Persist runners registration token in database
- Fix online editor should not remove newlines at the end of the file
+ - Expose Git's version in the admin area
+ - Show "New Merge Request" buttons on canonical repos when you have a fork (Josh Frye)
v 8.2.3
- Fix application settings cache not expiring after changes (Stan Hu)
@@ -144,6 +210,8 @@ v 8.2.0
- Allow to define cache in `.gitlab-ci.yml`
- Fix: 500 error returned if destroy request without HTTP referer (Kazuki Shimizu)
- Remove deprecated CI events from project settings page
+ - Use issue editor as cross reference comment author when issue is edited with a new mention.
+ - Add graphs of commits ahead and behind default branch (Jeff Stubler)
- Improve personal snippet access workflow (Douglas Alexandre)
- [API] Add ability to fetch the commit ID of the last commit that actually touched a file
- Fix omniauth documentation setting for omnibus configuration (Jon Cairns)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b9c2b3d2f8e..1eabbdc5cad 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -334,9 +334,9 @@ merge request:
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. [Markdown](http://www.cirosantilli.com/markdown-styleguide)
+1. [Documentation styleguide](doc/development/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
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index d48d3702aed..a04abec9149 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.9
+2.6.10
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 4b9fcbec101..7d8568351b4 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.5.1
+0.5.4
diff --git a/Gemfile b/Gemfile
index 2a1c4f7d73a..a9a8bed1064 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,6 +22,7 @@ gem 'devise', '~> 3.5.3'
gem 'devise-async', '~> 0.9.0'
gem 'doorkeeper', '~> 2.2.0'
gem 'omniauth', '~> 1.2.2'
+gem 'omniauth-azure-oauth2', '~> 0.0.6'
gem 'omniauth-bitbucket', '~> 0.0.2'
gem 'omniauth-cas3', '~> 1.1.2'
gem 'omniauth-facebook', '~> 3.0.0'
@@ -32,7 +33,7 @@ gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-saml', '~> 1.4.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'
-gem 'omniauth_crowd'
+gem 'omniauth_crowd', '~> 2.2.0'
gem 'rack-oauth2', '~> 1.2.1'
# reCAPTCHA protection
@@ -48,7 +49,7 @@ gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.2.20'
+gem "gitlab_git", '~> 7.2.22'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -66,10 +67,6 @@ gem 'grape', '~> 0.13.0'
gem 'grape-entity', '~> 0.4.2'
gem 'rack-cors', '~> 0.4.0', require: 'rack/cors'
-# Format dates and times
-# based on human-friendly examples
-gem "stamp", '~> 0.6.0'
-
# Pagination
gem "kaminari", "~> 0.16.3"
@@ -83,7 +80,7 @@ gem "carrierwave", '~> 0.9.0'
gem 'dropzonejs-rails', '~> 0.7.1'
# for aws storage
-gem "fog", "~> 1.25.0"
+gem "fog", "~> 1.36.0"
gem "unf", '~> 0.1.4'
# Authorization
@@ -169,10 +166,10 @@ gem 'asana', '~> 0.4.0'
gem 'ruby-fogbugz', '~> 0.2.1'
# d3
-gem 'd3_rails', '~> 3.5.5'
+gem 'd3_rails', '~> 3.5.0'
#cal-heatmap
-gem "cal-heatmap-rails", "~> 0.0.1"
+gem 'cal-heatmap-rails', '~> 3.5.0'
# underscore-rails
gem "underscore-rails", "~> 1.8.0"
@@ -200,7 +197,7 @@ gem 'turbolinks', '~> 2.5.0'
gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8'
-gem 'bootstrap-sass', '~> 3.0'
+gem 'bootstrap-sass', '~> 3.3.0'
gem 'font-awesome-rails', '~> 4.2'
gem 'gitlab_emoji', '~> 0.2.0'
gem 'gon', '~> 6.0.1'
@@ -250,7 +247,7 @@ group :development, :test do
gem 'byebug', platform: :mri
gem 'pry-rails'
- gem 'awesome_print', '~> 1.2.0'
+ gem 'awesome_print', '~> 1.2.0', require: false
gem 'fuubar', '~> 2.0.0'
gem 'database_cleaner', '~> 1.4.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index c4cadbafa26..f1bba7f437e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -49,7 +49,7 @@ GEM
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
- allocations (1.0.1)
+ allocations (1.0.3)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
@@ -66,7 +66,7 @@ GEM
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
- autoprefixer-rails (6.1.2)
+ autoprefixer-rails (6.2.3)
execjs
json
awesome_print (1.2.0)
@@ -82,9 +82,9 @@ GEM
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (3.3.5)
- autoprefixer-rails (>= 5.0.0.1)
- sass (>= 3.2.19)
+ bootstrap-sass (3.3.6)
+ autoprefixer-rails (>= 5.2.1)
+ sass (>= 3.3.4)
brakeman (3.1.4)
erubis (~> 2.6)
fastercsv (~> 1.5)
@@ -106,7 +106,7 @@ GEM
bundler (~> 1.2)
thor (~> 0.18)
byebug (8.2.1)
- cal-heatmap-rails (0.0.1)
+ cal-heatmap-rails (3.5.1)
capybara (2.4.4)
mime-types (>= 1.16)
nokogiri (>= 1.3.3)
@@ -219,21 +219,45 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog (1.25.0)
+ fog (1.36.0)
+ fog-aliyun (>= 0.1.0)
+ fog-atmos
+ fog-aws (>= 0.6.0)
fog-brightbox (~> 0.4)
- fog-core (~> 1.25)
+ fog-core (~> 1.32)
+ fog-dynect (~> 0.0.2)
+ fog-ecloud (~> 0.1)
+ fog-google (<= 0.1.0)
fog-json
+ fog-local
+ fog-powerdns (>= 0.1.1)
fog-profitbricks
fog-radosgw (>= 0.0.2)
+ fog-riakcs
fog-sakuracloud (>= 0.0.4)
+ fog-serverlove
fog-softlayer
+ fog-storm_on_demand
fog-terremark
fog-vmfusion
fog-voxel
+ fog-xenserver
fog-xml (~> 0.1.1)
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
- opennebula
+ fog-aliyun (0.1.0)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ ipaddress (~> 0.8)
+ xml-simple (~> 1.1)
+ fog-atmos (0.1.0)
+ fog-core
+ fog-xml
+ fog-aws (0.8.1)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ fog-xml (~> 0.1)
+ ipaddress (~> 0.8)
fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
@@ -242,21 +266,48 @@ GEM
builder
excon (~> 0.45)
formatador (~> 0.2)
+ fog-dynect (0.0.2)
+ fog-core
+ fog-json
+ fog-xml
+ fog-ecloud (0.3.0)
+ fog-core
+ fog-xml
+ fog-google (0.1.0)
+ fog-core
+ fog-json
+ fog-xml
fog-json (1.0.2)
fog-core (~> 1.0)
multi_json (~> 1.10)
+ fog-local (0.2.1)
+ fog-core (~> 1.27)
+ fog-powerdns (0.1.1)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ fog-xml (~> 0.1)
fog-profitbricks (0.0.5)
fog-core
fog-xml
nokogiri
- fog-radosgw (0.0.4)
+ fog-radosgw (0.0.5)
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
- fog-sakuracloud (1.5.0)
+ fog-riakcs (0.1.0)
+ fog-core
+ fog-json
+ fog-xml
+ fog-sakuracloud (1.7.5)
fog-core
fog-json
- fog-softlayer (1.0.2)
+ fog-serverlove (0.1.2)
+ fog-core
+ fog-json
+ fog-softlayer (1.0.3)
+ fog-core
+ fog-json
+ fog-storm_on_demand (0.1.1)
fog-core
fog-json
fog-terremark (0.1.0)
@@ -268,6 +319,9 @@ GEM
fog-voxel (0.1.0)
fog-core
fog-xml
+ fog-xenserver (0.2.2)
+ fog-core
+ fog-xml
fog-xml (0.1.2)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
@@ -377,7 +431,7 @@ GEM
influxdb (0.2.3)
cause
json
- ipaddress (0.8.0)
+ ipaddress (0.8.2)
jquery-atwho-rails (1.3.2)
jquery-rails (4.0.5)
rails-dom-testing (~> 1.0)
@@ -443,6 +497,10 @@ GEM
omniauth (1.2.2)
hashie (>= 1.2, < 4)
rack (~> 1.0)
+ omniauth-azure-oauth2 (0.0.6)
+ jwt (~> 1.0)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
omniauth-bitbucket (0.0.2)
multi_json (~> 1.7)
omniauth (~> 1.1)
@@ -488,10 +546,6 @@ GEM
activesupport
nokogiri (>= 1.4.4)
omniauth (~> 1.0)
- opennebula (4.14.2)
- json
- nokogiri
- rbvmomi
org-ruby (0.9.12)
rubypants (~> 0.2)
orm_adapter (0.5.0)
@@ -567,10 +621,6 @@ GEM
ffi (>= 0.5.0)
rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3)
- rbvmomi (1.8.2)
- builder
- nokogiri (>= 1.4.1)
- trollop
rdoc (3.12.2)
json (~> 1.4)
recaptcha (1.0.2)
@@ -730,7 +780,6 @@ GEM
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
- stamp (0.6.0)
state_machines (0.4.0)
state_machines-activemodel (0.3.0)
activemodel (~> 4.1)
@@ -770,7 +819,6 @@ GEM
multi_json (~> 1.7)
twitter-stream (~> 0.1)
tins (1.6.0)
- trollop (2.1.2)
turbolinks (2.5.3)
coffee-rails
twitter-stream (0.1.16)
@@ -819,6 +867,7 @@ GEM
builder
expression_parser
rinku
+ xml-simple (1.1.5)
xpath (2.0.0)
nokogiri (~> 1.3)
@@ -843,13 +892,13 @@ DEPENDENCIES
benchmark-ips
better_errors (~> 1.0.1)
binding_of_caller (~> 0.7.2)
- bootstrap-sass (~> 3.0)
+ bootstrap-sass (~> 3.3.0)
brakeman (~> 3.1.0)
browser (~> 1.0.0)
bullet
bundler-audit
byebug
- cal-heatmap-rails (~> 0.0.1)
+ cal-heatmap-rails (~> 3.5.0)
capybara (~> 2.4.0)
capybara-screenshot (~> 1.0.0)
carrierwave (~> 0.9.0)
@@ -859,7 +908,7 @@ DEPENDENCIES
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
- d3_rails (~> 3.5.5)
+ d3_rails (~> 3.5.0)
database_cleaner (~> 1.4.0)
default_value_for (~> 3.0.0)
devise (~> 3.5.3)
@@ -874,7 +923,7 @@ DEPENDENCIES
ffaker (~> 2.0.0)
flay
flog
- fog (~> 1.25.0)
+ fog (~> 1.36.0)
font-awesome-rails (~> 4.2)
foreman
fuubar (~> 2.0.0)
@@ -883,7 +932,7 @@ DEPENDENCIES
github-markup (~> 1.3.1)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab_emoji (~> 0.2.0)
- gitlab_git (~> 7.2.20)
+ gitlab_git (~> 7.2.22)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.1.0)
@@ -916,6 +965,7 @@ DEPENDENCIES
oauth2 (~> 1.0.0)
octokit (~> 3.7.0)
omniauth (~> 1.2.2)
+ omniauth-azure-oauth2 (~> 0.0.6)
omniauth-bitbucket (~> 0.0.2)
omniauth-cas3 (~> 1.1.2)
omniauth-facebook (~> 3.0.0)
@@ -926,7 +976,7 @@ DEPENDENCIES
omniauth-saml (~> 1.4.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)
- omniauth_crowd
+ omniauth_crowd (~> 2.2.0)
org-ruby (~> 0.9.12)
paranoia (~> 2.0)
pg (~> 0.18.2)
@@ -973,7 +1023,6 @@ DEPENDENCIES
spring-commands-spinach (~> 1.0.0)
spring-commands-teaspoon (~> 0.0.2)
sprockets (~> 2.12.3)
- stamp (~> 0.6.0)
state_machines-activerecord (~> 0.3.0)
task_list (~> 1.0.2)
teaspoon (~> 1.0.0)
@@ -994,4 +1043,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.10.6
+ 1.11.2
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/Procfile b/Procfile
index bbafdd33a2d..9cfdee7040f 100644
--- a/Procfile
+++ b/Procfile
@@ -3,5 +3,5 @@
# lib/support/init.d, which call scripts in bin/ .
#
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
-worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -q metrics
+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/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt
index a9b845ed1d4..df187637e18 100755
--- a/app/assets/fonts/OFL.txt
+++ b/app/assets/fonts/OFL.txt
@@ -1,7 +1,8 @@
-Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
-This license is copied below, and is also available with a FAQ at:
-http://scripts.sil.org/OFL
+
+This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
deleted file mode 100644
index 9c9b5cb7f03..00000000000
--- a/app/assets/fonts/SourceSansPro-Black.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf.woff b/app/assets/fonts/SourceSansPro-Black.ttf.woff
new file mode 100755
index 00000000000..b7e86200927
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Black.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf b/app/assets/fonts/SourceSansPro-BlackIt.ttf
deleted file mode 100644
index 294ce5abe8f..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
new file mode 100755
index 00000000000..c3314b1ef06
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackItalic.ttf b/app/assets/fonts/SourceSansPro-BlackItalic.ttf
deleted file mode 100755
index c719243c0d6..00000000000
--- a/app/assets/fonts/SourceSansPro-BlackItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
deleted file mode 100644
index 5d65c93242f..00000000000
--- a/app/assets/fonts/SourceSansPro-Bold.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf.woff b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
new file mode 100755
index 00000000000..d1d40f840f8
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf b/app/assets/fonts/SourceSansPro-BoldIt.ttf
deleted file mode 100644
index 3decd130070..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
new file mode 100755
index 00000000000..ef6ff514d3a
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldItalic.ttf b/app/assets/fonts/SourceSansPro-BoldItalic.ttf
deleted file mode 100755
index d20dd0c5eca..00000000000
--- a/app/assets/fonts/SourceSansPro-BoldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
deleted file mode 100644
index 253eafa3783..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLight.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
new file mode 100755
index 00000000000..1e6c94d9eb3
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
deleted file mode 100644
index 00d7e9a7aa8..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
new file mode 100755
index 00000000000..7a408b1ec73
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
deleted file mode 100755
index 2c34f3b8dc4..00000000000
--- a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf b/app/assets/fonts/SourceSansPro-It.ttf
deleted file mode 100644
index f7af5377595..00000000000
--- a/app/assets/fonts/SourceSansPro-It.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-It.ttf.woff b/app/assets/fonts/SourceSansPro-It.ttf.woff
new file mode 100755
index 00000000000..4d54bc95718
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-It.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Italic.ttf b/app/assets/fonts/SourceSansPro-Italic.ttf
deleted file mode 100755
index e5a1a86e631..00000000000
--- a/app/assets/fonts/SourceSansPro-Italic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
deleted file mode 100644
index 83a0a336661..00000000000
--- a/app/assets/fonts/SourceSansPro-Light.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf.woff b/app/assets/fonts/SourceSansPro-Light.ttf.woff
new file mode 100755
index 00000000000..1706d57d3c5
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Light.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf b/app/assets/fonts/SourceSansPro-LightIt.ttf
deleted file mode 100644
index f18827985ef..00000000000
--- a/app/assets/fonts/SourceSansPro-LightIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightIt.ttf.woff b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
new file mode 100755
index 00000000000..87378d6c609
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightItalic.ttf b/app/assets/fonts/SourceSansPro-LightItalic.ttf
deleted file mode 100755
index 88a6778d24f..00000000000
--- a/app/assets/fonts/SourceSansPro-LightItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
deleted file mode 100644
index 44486cdc670..00000000000
--- a/app/assets/fonts/SourceSansPro-Regular.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf.woff b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
new file mode 100755
index 00000000000..460ab12a638
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
deleted file mode 100644
index 86b00c067e0..00000000000
--- a/app/assets/fonts/SourceSansPro-Semibold.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf.woff b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
new file mode 100755
index 00000000000..43379631b2d
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
deleted file mode 100644
index 13d66a1fc45..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
new file mode 100755
index 00000000000..232c2048ae7
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldIt.ttf.woff
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
deleted file mode 100755
index 2c5ad3008c3..00000000000
--- a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png
new file mode 100644
index 00000000000..a82c751e001
--- /dev/null
+++ b/app/assets/images/auth_buttons/azure_64.png
Binary files differ
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 63803747413..3b6b453ac51 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event-filter .btn").bind "click", (event) =>
+ $(".event-filter a").bind "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,7 +12,7 @@ class @Activities
toggleFilter: (sender) ->
- sender.toggleClass "active"
+ sender.closest('li').toggleClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
if event_filters
diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee
index bcb2e6df7c0..eb951f71711 100644
--- a/app/assets/javascripts/admin.js.coffee
+++ b/app/assets/javascripts/admin.js.coffee
@@ -10,19 +10,19 @@ class @Admin
$('body').on 'click', '.js-toggle-colors-link', (e) ->
e.preventDefault()
- $('.js-toggle-colors-link').hide()
- $('.js-toggle-colors-container').show()
+ $('.js-toggle-colors-container').toggle()
$('input#broadcast_message_color').on 'input', ->
- previewColor = $('input#broadcast_message_color').val()
+ previewColor = $(@).val()
$('div.broadcast-message-preview').css('background-color', previewColor)
$('input#broadcast_message_font').on 'input', ->
- previewColor = $('input#broadcast_message_font').val()
+ previewColor = $(@).val()
$('div.broadcast-message-preview').css('color', previewColor)
$('textarea#broadcast_message_message').on 'input', ->
- previewMessage = $('textarea#broadcast_message_message').val()
+ previewMessage = $(@).val()
+ previewMessage = "Your message here" if previewMessage.trim() == ''
$('div.broadcast-message-preview span').text(previewMessage)
$('.log-tabs a').click (e) ->
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index affab5bb030..c095e5ae2b1 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -10,12 +10,12 @@
#= 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,9 +38,9 @@
#= require shortcuts_dashboard_navigation
#= require shortcuts_issuable
#= require shortcuts_network
-#= require cal-heatmap
#= require jquery.nicescroll.min
#= require_tree .
+#= require fuzzaldrin-plus.min
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
diff --git a/app/assets/javascripts/behaviors/autosize.js.coffee b/app/assets/javascripts/behaviors/autosize.js.coffee
new file mode 100644
index 00000000000..b32072e61ee
--- /dev/null
+++ b/app/assets/javascripts/behaviors/autosize.js.coffee
@@ -0,0 +1,4 @@
+#= require autosize
+
+$ ->
+ autosize($('.js-autosize'))
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee
index 917228bd276..f2fd2a775a4 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/branch-graph.js.coffee
@@ -66,7 +66,7 @@ class @BranchGraph
r.rect(40, 0, 30, @barHeight).attr fill: "#444"
for day, mm in @days
- if cuday isnt day[0]
+ if cuday isnt day[0] || cumonth isnt day[1]
# Dates
r.text(55, @offsetY + @unitTime * mm, day[0])
.attr(
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 97621236924..d80e0e716ce 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -1,9 +1,4 @@
class @Calendar
- options =
- month: "short"
- day: "numeric"
- year: "numeric"
-
constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap()
cal.init
diff --git a/app/assets/javascripts/commits.js.coffee b/app/assets/javascripts/commits.js.coffee
index c183e78e513..ffd3627b1b0 100644
--- a/app/assets/javascripts/commits.js.coffee
+++ b/app/assets/javascripts/commits.js.coffee
@@ -1,15 +1,5 @@
class @CommitsList
- @data =
- ref: null
- limit: 0
- offset: 0
- @disable = false
-
- @showProgress: ->
- $('.loading').show()
-
- @hideProgress: ->
- $('.loading').hide()
+ @timer = null
@init: (ref, limit) ->
$("body").on "click", ".day-commits-table li.commit", (event) ->
@@ -18,38 +8,32 @@ class @CommitsList
e.stopPropagation()
return false
- @data.ref = ref
- @data.limit = limit
- @data.offset = limit
+ Pager.init limit, false
+
+ @content = $("#commits-list")
+ @searchField = $("#commits-search")
+ @initSearch()
- this.initLoadMore()
- this.showProgress()
+ @initSearch: ->
+ @timer = null
+ @searchField.keyup =>
+ clearTimeout(@timer)
+ @timer = setTimeout(@filterResults, 500)
+
+ @filterResults: =>
+ form = $(".commits-search-form")
+ search = @searchField.val()
+ commitsUrl = form.attr("action") + '?' + form.serialize()
+ @content.fadeTo('fast', 0.5)
- @getOld: ->
- this.showProgress()
$.ajax
type: "GET"
- url: location.href
- data: @data
- complete: this.hideProgress
- success: (data) ->
- CommitsList.append(data.count, data.html)
+ url: form.attr("action")
+ data: form.serialize()
+ complete: =>
+ @content.fadeTo('fast', 1.0)
+ success: (data) =>
+ @content.html(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: commitsUrl}, document.title, commitsUrl
dataType: "json"
-
- @append: (count, html) ->
- $("#commits-list").append(html)
- if count > 0
- @data.offset += count
- else
- @disable = true
-
- @initLoadMore: ->
- $(document).unbind('scroll')
- $(document).endlessScroll
- bottomPixels: 400
- fireDelay: 1000
- fireOnce: true
- ceaseFire: =>
- @disable
- callback: =>
- this.getOld()
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 69e061ce6e9..58d6b9d4060 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -87,7 +87,9 @@ class Dispatcher
new GroupAvatar()
when 'projects:tree:show'
new TreeView()
- shortcut_handler = new ShortcutsNavigation()
+ shortcut_handler = new ShortcutsTree()
+ when 'projects:find_file:show'
+ shortcut_handler = true
when 'projects:blob:show'
new LineHighlighter()
shortcut_handler = new ShortcutsNavigation()
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 30a35a04339..c714c0fa939 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -66,7 +66,7 @@ class @DropzoneInput
success: (header, response) ->
child = $(dropzone[0]).children("textarea")
- $(child).val $(child).val() + formatLink(response.link) + "\n"
+ $(child).val $(child).val() + response.link.markdown + "\n"
return
error: (temp, errorMessage) ->
@@ -99,11 +99,6 @@ class @DropzoneInput
child = $(dropzone[0]).children("textarea")
- formatLink = (link) ->
- text = "[#{link.alt}](#{link.url})"
- text = "!#{text}" if link.is_image
- text
-
handlePaste = (event) ->
pasteEvent = event.originalEvent
if pasteEvent.clipboardData and pasteEvent.clipboardData.items
@@ -162,7 +157,7 @@ class @DropzoneInput
closeAlertMessage()
success: (e, textStatus, response) ->
- insertToTextArea(filename, formatLink(response.responseJSON.link))
+ insertToTextArea(filename, response.responseJSON.link.markdown)
error: (response) ->
showError(response.responseJSON.message)
@@ -202,8 +197,3 @@ class @DropzoneInput
e.preventDefault()
$(@).closest('.gfm-form').find('.div-dropzone').click()
return
-
- formatLink: (link) ->
- text = "[#{link.alt}](#{link.url})"
- text = "!#{text}" if link.is_image
- text
diff --git a/app/assets/javascripts/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 7967892f856..4718bcf7a1e 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -34,7 +34,7 @@ GitLab.GfmAutoComplete =
searchKey: 'search'
callbacks:
beforeSave: (members) ->
- $.map members, (m) ->
+ $.map members, (m) ->
title = m.name
title += " (#{m.count})" if m.count
@@ -50,7 +50,7 @@ GitLab.GfmAutoComplete =
insertTpl: '${atwho-at}${id}'
callbacks:
beforeSave: (issues) ->
- $.map issues, (i) ->
+ $.map issues, (i) ->
id: i.iid
title: sanitize(i.title)
search: "#{i.iid} #{i.title}"
@@ -63,12 +63,12 @@ GitLab.GfmAutoComplete =
insertTpl: '${atwho-at}${id}'
callbacks:
beforeSave: (merges) ->
- $.map merges, (m) ->
+ $.map merges, (m) ->
id: m.iid
title: sanitize(m.title)
search: "#{m.iid} #{m.title}"
- input.one 'focus', =>
+ if @dataSource
$.getJSON(@dataSource).done (data) ->
# load members
input.atwho 'load', '@', data.members
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index c256ec8f41b..0d26c58a81d 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -16,12 +16,16 @@ class @Issue
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
initIssueBtnEventListeners: ->
+ _this = @
issueFailMessage = 'Unable to update this issue at this time.'
$('a.btn-close, a.btn-reopen').on 'click', (e) ->
e.preventDefault()
e.stopImmediatePropagation()
$this = $(this)
isClose = $this.hasClass('btn-close')
+ shouldSubmit = $this.hasClass('btn-comment')
+ if shouldSubmit
+ _this.submitNoteForm($this.closest('form'))
$this.prop('disabled', true)
url = $this.attr('href')
$.ajax
@@ -32,12 +36,13 @@ class @Issue
new Flash(issueFailMessage, 'alert')
success: (data, textStatus, jqXHR) ->
if data.saved
- $this.addClass('hidden')
if isClose
+ $('a.btn-close').addClass('hidden')
$('a.btn-reopen').removeClass('hidden')
$('div.status-box-closed').removeClass('hidden')
$('div.status-box-open').addClass('hidden')
else
+ $('a.btn-reopen').addClass('hidden')
$('a.btn-close').removeClass('hidden')
$('div.status-box-closed').addClass('hidden')
$('div.status-box-open').removeClass('hidden')
@@ -45,6 +50,11 @@ class @Issue
new Flash(issueFailMessage, 'alert')
$this.prop('disabled', false)
+ submitNoteForm: (form) =>
+ noteText = form.find("textarea.js-note-text").val()
+ if noteText.trim().length > 0
+ form.submit()
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index ac9e022e727..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()
@@ -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..a5879c8b793
--- /dev/null
+++ b/app/assets/javascripts/logo.js.coffee
@@ -0,0 +1,44 @@
+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
+ clearInterval(currentTimer) if currentTimer
+ currentTimer = setInterval(work, delay)
+
+stop = ->
+ clearInterval(currentTimer)
+ clearHighlights()
+
+work = ->
+ clearHighlights()
+ $(pieces[pieceIndex]).attr('class', "#{defaultClass} highlight")
+
+ # If we hit the last piece, reset the index and then reverse the array to
+ # get a nice back-and-forth sweeping look
+ if pieceIndex == pieces.length - 1
+ pieceIndex = 0
+ pieces.reverse()
+ else
+ pieceIndex++
+
+$(document).on('page:fetch', start)
+$(document).on('page:change', stop)
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 9047587db81..1f46e331427 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -19,6 +19,7 @@ class @MergeRequest
# Prevent duplicate event bindings
@disableTaskList()
+ @initMRBtnListeners()
if $("a.btn-close").length
@initTaskList()
@@ -43,6 +44,28 @@ class @MergeRequest
$('.detail-page-description .js-task-list-container').taskList('enable')
$(document).on 'tasklist:changed', '.detail-page-description .js-task-list-container', @updateTaskList
+ initMRBtnListeners: ->
+ _this = @
+ $('a.btn-close, a.btn-reopen').on 'click', (e) ->
+ $this = $(this)
+ shouldSubmit = $this.hasClass('btn-comment')
+ if shouldSubmit && $this.data('submitted')
+ return
+ if shouldSubmit
+ if $this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')
+ e.preventDefault()
+ e.stopImmediatePropagation()
+ _this.submitNoteForm($this.closest('form'),$this)
+
+
+ submitNoteForm: (form, $button) =>
+ noteText = form.find("textarea.js-note-text").val()
+ if noteText.trim().length > 0
+ form.submit()
+ $button.data('submitted',true)
+ $button.trigger('click')
+
+
disableTaskList: ->
$('.detail-page-description .js-task-list-container').taskList('disable')
$(document).off 'tasklist:changed', '.detail-page-description .js-task-list-container'
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 9e2dc1250c9..b10e1db7f3f 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -5,7 +5,7 @@
#
# ### Example Markup
#
-# <ul class="nav nav-tabs merge-request-tabs">
+# <ul class="nav-links merge-request-tabs">
# <li class="notes-tab active">
# <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
# Discussion
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/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 9e5204bfeeb..356fb6aa08c 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -1,4 +1,5 @@
#= require autosave
+#= require autosize
#= require dropzone
#= require dropzone_input
#= require gfm_auto_complete
@@ -33,8 +34,6 @@ class @Notes
$(document).on "click", ".note-edit-cancel", @cancelEdit
# Reopen and close actions for Issue/MR combined with note form submit
- $(document).on "click", ".js-note-target-reopen", @targetReopen
- $(document).on "click", ".js-note-target-close", @targetClose
$(document).on "click", ".js-comment-button", @updateCloseButton
$(document).on "keyup", ".js-note-text", @updateTargetButtons
@@ -248,6 +247,7 @@ class @Notes
else
previewButton.removeClass("turn-on").addClass "turn-off"
+ autosize(textarea)
new Autosave textarea, [
"Note"
form.find("#note_commit_id").val()
@@ -512,17 +512,6 @@ class @Notes
visibilityChange: =>
@refresh()
- targetReopen: (e) =>
- @submitNoteForm($(e.target).parents('form'))
-
- targetClose: (e) =>
- @submitNoteForm($(e.target).parents('form'))
-
- submitNoteForm: (form) =>
- noteText = form.find(".js-note-text").val()
- if noteText.trim().length > 0
- form.submit()
-
updateCloseButton: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
@@ -531,13 +520,16 @@ class @Notes
updateTargetButtons: (e) =>
textarea = $(e.target)
form = textarea.parents('form')
-
if textarea.val().trim().length > 0
form.find('.js-note-target-reopen').text('Comment & reopen')
form.find('.js-note-target-close').text('Comment & close')
+ form.find('.js-note-target-reopen').addClass('btn-comment-and-reopen')
+ form.find('.js-note-target-close').addClass('btn-comment-and-close')
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
+ form.find('.js-note-target-reopen').removeClass('btn-comment-and-reopen')
+ form.find('.js-note-target-close').removeClass('btn-comment-and-close')
initTaskList: ->
@enableTaskList()
diff --git a/app/assets/javascripts/project_find_file.js.coffee b/app/assets/javascripts/project_find_file.js.coffee
new file mode 100644
index 00000000000..0dd32352c34
--- /dev/null
+++ b/app/assets/javascripts/project_find_file.js.coffee
@@ -0,0 +1,125 @@
+class @ProjectFindFile
+ constructor: (@element, @options)->
+ @filePaths = {}
+ @inputElement = @element.find(".file-finder-input")
+
+ # init event
+ @initEvent()
+
+ # focus text input box
+ @inputElement.focus()
+
+ # load file list
+ @load(@options.url)
+
+ # init event
+ initEvent: ->
+ @inputElement.off "keyup"
+ @inputElement.on "keyup", (event) =>
+ target = $(event.target)
+ value = target.val()
+ oldValue = target.data("oldValue") ? ""
+
+ if value != oldValue
+ target.data("oldValue", value)
+ @findFile()
+ @element.find("tr.tree-item").eq(0).addClass("selected").focus()
+
+ @element.find(".tree-content-holder .tree-table").on "click", (event) ->
+ if (event.target.nodeName != "A")
+ path = @element.find(".tree-item-file-name a", this).attr("href")
+ location.href = path if path
+
+ # find file
+ findFile: ->
+ searchText = @inputElement.val()
+ result = if searchText.length > 0 then fuzzaldrinPlus.filter(@filePaths, searchText) else @filePaths
+ @renderList result, searchText
+
+ # files pathes load
+ load: (url) ->
+ $.ajax
+ url: url
+ method: "get"
+ dataType: "json"
+ success: (data) =>
+ @element.find(".loading").hide()
+ @filePaths = data
+ @findFile()
+ @element.find(".files-slider tr.tree-item").eq(0).addClass("selected").focus()
+
+ # render result
+ renderList: (filePaths, searchText) ->
+ @element.find(".tree-table > tbody").empty()
+
+ for filePath, i in filePaths
+ break if i == 20
+
+ if searchText
+ matches = fuzzaldrinPlus.match(filePath, searchText)
+
+ blobItemUrl = "#{@options.blobUrlTemplate}/#{filePath}"
+
+ html = @makeHtml filePath, matches, blobItemUrl
+ @element.find(".tree-table > tbody").append(html)
+
+ # highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
+ highlighter = (element, text, matches) ->
+ lastIndex = 0
+ highlightText = ""
+ matchedChars = []
+
+ for matchIndex in matches
+ unmatched = text.substring(lastIndex, matchIndex)
+
+ if unmatched
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ matchedChars = []
+ element.append(document.createTextNode(unmatched))
+
+ matchedChars.push(text[matchIndex])
+ lastIndex = matchIndex + 1
+
+ element.append(matchedChars.join("").bold()) if matchedChars.length
+ element.append(document.createTextNode(text.substring(lastIndex)))
+
+ # make tbody row html
+ makeHtml: (filePath, matches, blobItemUrl) ->
+ $tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>")
+ if matches
+ $tr.find("a").replaceWith(highlighter($tr.find("a"), filePath, matches).attr("href", blobItemUrl))
+ else
+ $tr.find("a").attr("href", blobItemUrl).text(filePath)
+
+ return $tr
+
+ selectRow: (type) ->
+ rows = @element.find(".files-slider tr.tree-item")
+ selectedRow = @element.find(".files-slider tr.tree-item.selected")
+
+ if rows && rows.length > 0
+ if selectedRow && selectedRow.length > 0
+ if type == "UP"
+ next = selectedRow.prev()
+ else if type == "DOWN"
+ next = selectedRow.next()
+
+ if next.length > 0
+ selectedRow.removeClass "selected"
+ selectedRow = next
+ else
+ selectedRow = rows.eq(0)
+ selectedRow.addClass("selected").focus()
+
+ selectRowUp: =>
+ @selectRow "UP"
+
+ selectRowDown: =>
+ @selectRow "DOWN"
+
+ goToTree: =>
+ location.href = @options.treeUrl
+
+ goToBlob: =>
+ path = @element.find(".tree-item.selected .tree-item-file-name a").attr("href")
+ location.href = path if path
diff --git a/app/assets/javascripts/shortcuts_find_file.js.coffee b/app/assets/javascripts/shortcuts_find_file.js.coffee
new file mode 100644
index 00000000000..311e80bae19
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_find_file.js.coffee
@@ -0,0 +1,19 @@
+#= require shortcuts_navigation
+
+class @ShortcutsFindFile extends ShortcutsNavigation
+ constructor: (@projectFindFile) ->
+ super()
+ _oldStopCallback = Mousetrap.stopCallback
+ # override to fire shortcuts action when focus in textbox
+ Mousetrap.stopCallback = (event, element, combo) =>
+ if element == @projectFindFile.inputElement[0] and (combo == 'up' or combo == 'down' or combo == 'esc' or combo == 'enter')
+ # when press up/down key in textbox, cusor prevent to move to home/end
+ event.preventDefault()
+ return false
+
+ return _oldStopCallback(event, element, combo)
+
+ Mousetrap.bind('up', @projectFindFile.selectRowUp)
+ Mousetrap.bind('down', @projectFindFile.selectRowDown)
+ Mousetrap.bind('esc', @projectFindFile.goToTree)
+ Mousetrap.bind('enter', @projectFindFile.goToBlob)
diff --git a/app/assets/javascripts/shortcuts_tree.coffee b/app/assets/javascripts/shortcuts_tree.coffee
new file mode 100644
index 00000000000..ba0839c9fc0
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_tree.coffee
@@ -0,0 +1,4 @@
+class @ShortcutsTree extends ShortcutsNavigation
+ constructor: ->
+ super()
+ Mousetrap.bind('t', -> ShortcutsTree.findAndFollowLink('.shortcuts-find-file'))
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 12abf806bfa..9467011799f 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -117,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/javascripts/wikis.js.coffee b/app/assets/javascripts/wikis.js.coffee
index 81cfc37b956..19420f42468 100644
--- a/app/assets/javascripts/wikis.js.coffee
+++ b/app/assets/javascripts/wikis.js.coffee
@@ -1,17 +1,18 @@
+#= require latinise
+
class @Wikis
constructor: ->
- $('.build-new-wiki').bind "click", (e) ->
- $('[data-error~=slug]').addClass("hidden")
- $('p.hint').show()
+ $('.build-new-wiki').bind 'click', (e) =>
+ $('[data-error~=slug]').addClass('hidden')
field = $('#new_wiki_path')
- valid_slug_pattern = /^[\w\/-]+$/
+ slug = @slugify(field.val())
- slug = field.val()
- if slug.match valid_slug_pattern
+ if (slug.length > 0)
path = field.attr('data-wikis-path')
- if(slug.length > 0)
- location.href = path + "/" + slug
- else
- e.preventDefault()
- $('p.hint').hide()
- $('[data-error~=slug]').removeClass("hidden")
+ location.href = path + '/' + slug
+
+ dasherize: (value) ->
+ value.replace(/[_\s]+/g, '-')
+
+ slugify: (value) =>
+ @dasherize(value.trim().toLowerCase().latinise())
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
index a1462cf3cae..e1c5446eaac 100644
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ b/app/assets/javascripts/zen_mode.js.coffee
@@ -1,56 +1,80 @@
+# Zen Mode (full screen) textarea
+#
+#= provides zen_mode:enter
+#= provides zen_mode:leave
+#
+#= require jquery.scrollTo
#= require dropzone
#= require mousetrap
#= require mousetrap/pause
-
+#
+# ### Events
+#
+# `zen_mode:enter`
+#
+# Fired when the "Edit in fullscreen" link is clicked.
+#
+# **Synchronicity** Sync
+# **Bubbles** Yes
+# **Cancelable** No
+# **Target** a.js-zen-enter
+#
+# `zen_mode:leave`
+#
+# Fired when the "Leave Fullscreen" link is clicked.
+#
+# **Synchronicity** Sync
+# **Bubbles** Yes
+# **Cancelable** No
+# **Target** a.js-zen-leave
+#
class @ZenMode
constructor: ->
- @active_zen_area = null
- @active_checkbox = null
- @scroll_position = 0
-
- $(window).scroll =>
- if not @active_checkbox
- @scroll_position = window.pageYOffset
+ @active_backdrop = null
+ @active_textarea = null
- $('body').on 'click', '.zen-enter-link', (e) =>
+ $(document).on 'click', '.js-zen-enter', (e) ->
e.preventDefault()
- $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', true).change()
+ $(e.currentTarget).trigger('zen_mode:enter')
- $('body').on 'click', '.zen-leave-link', (e) =>
+ $(document).on 'click', '.js-zen-leave', (e) ->
e.preventDefault()
- $(e.currentTarget).closest('.zennable').find('.zen-toggle-comment').prop('checked', false).change()
-
- $('body').on 'change', '.zen-toggle-comment', (e) =>
- checkbox = e.currentTarget
- if checkbox.checked
- # Disable other keyboard shortcuts in ZEN mode
- Mousetrap.pause()
- @updateActiveZenArea(checkbox)
- else
- @exitZenMode()
-
- $(document).on 'keydown', (e) =>
- if e.keyCode is 27 # Esc
- @exitZenMode()
+ $(e.currentTarget).trigger('zen_mode:leave')
+
+ $(document).on 'zen_mode:enter', (e) =>
+ @enter(e.target.parentNode)
+ $(document).on 'zen_mode:leave', (e) =>
+ @exit()
+
+ $(document).on 'keydown', (e) ->
+ if e.keyCode == 27 # Esc
e.preventDefault()
+ $(document).trigger('zen_mode:leave')
+
+ enter: (backdrop) ->
+ Mousetrap.pause()
+
+ @active_backdrop = $(backdrop)
+ @active_backdrop.addClass('fullscreen')
+
+ @active_textarea = @active_backdrop.find('textarea')
- updateActiveZenArea: (checkbox) =>
- @active_checkbox = $(checkbox)
- @active_checkbox.prop('checked', true)
- @active_zen_area = @active_checkbox.parent().find('textarea')
# Prevent a user-resized textarea from persisting to fullscreen
- @active_zen_area.removeAttr('style')
- @active_zen_area.focus()
+ @active_textarea.removeAttr('style')
+ @active_textarea.focus()
- exitZenMode: =>
- if @active_zen_area isnt null
+ exit: ->
+ if @active_textarea
Mousetrap.unpause()
- @active_checkbox.prop('checked', false)
- @active_zen_area = null
- @active_checkbox = null
- @restoreScroll(@scroll_position)
- # Enable dropzone when leaving ZEN mode
+
+ @active_textarea.closest('.zen-backdrop').removeClass('fullscreen')
+
+ @scrollTo(@active_textarea)
+
+ @active_textarea = null
+ @active_backdrop = null
+
Dropzone.forElement('.div-dropzone').enable()
- restoreScroll: (y) ->
- window.scrollTo(window.pageXOffset, y)
+ scrollTo: (zen_area) ->
+ $.scrollTo(zen_area, 0, offset: -150)
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 48a4971c8fc..fa7641b1676 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,6 +24,7 @@
@import "framework/lists.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
+@import "framework/nav.scss";
@import "framework/pagination.scss";
@import "framework/panels.scss";
@import "framework/selects.scss";
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 206d39cc9b3..d0f5d33bf4d 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -18,9 +18,9 @@
line-height: 36px;
}
-.content-block,
.gray-content-block {
- margin: -$gl-padding;
+ margin-top: 0;
+ margin-bottom: -$gl-padding;
background-color: $background-color;
padding: $gl-padding;
margin-bottom: 0px;
@@ -72,15 +72,21 @@
> p:last-child {
margin-bottom: 0;
}
+
+ .block-controls {
+ float: right;
+
+ .control {
+ float: left;
+ margin-left: 10px;
+ }
+ }
}
.cover-block {
text-align: center;
background: $background-color;
- margin: -$gl-padding;
- margin-bottom: 0;
- padding: 44px $gl-padding;
- border-bottom: 1px solid $border-color;
+ padding-top: 44px;
position: relative;
.avatar-holder {
@@ -127,3 +133,19 @@
.block-connector {
margin-top: -1px;
}
+
+.nav-block {
+ .controls {
+ float: right;
+ margin-top: 11px;
+ }
+}
+
+.content-block {
+ padding: $gl-padding 0;
+ border-bottom: 1px solid $border-color;
+
+ &.oneline-block {
+ line-height: 42px;
+ }
+}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 97a94638847..c99292c3f83 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,12 +1,8 @@
@mixin btn-default {
@include border-radius(3px);
- border-width: 1px;
- border-style: solid;
- font-size: 15px;
+ font-size: $gl-font-size;
font-weight: 500;
- line-height: 18px;
- padding: 11px $gl-padding;
- letter-spacing: .4px;
+ padding: $gl-vert-padding $gl-padding;
&:focus,
&:active {
@@ -17,8 +13,6 @@
@mixin btn-middle {
@include btn-default;
- @include border-radius(3px);
- padding: 11px 24px;
}
@mixin btn-color($light, $border-light, $normal, $border-normal, $dark, $border-dark, $color) {
@@ -74,16 +68,15 @@
@include btn-default;
@include btn-white;
+ &.btn-small,
&.btn-sm {
- padding: 5px 10px;
- }
-
- &.btn-nr {
- padding: 7px 10px;
+ padding: 4px 10px;
+ font-size: 13px;
+ line-height: 18px;
}
&.btn-xs {
- padding: 1px 5px;
+ padding: 2px 5px;
}
&.btn-success,
@@ -131,6 +124,12 @@
&:last-child {
margin-right: 0px;
}
+ &.btn-xs {
+ margin-right: 3px;
+ }
+ }
+ &.disabled {
+ pointer-events: auto !important;
}
}
@@ -153,33 +152,42 @@
}
}
-.btn-group-next {
+.btn-clipboard {
+ border: none;
+ padding: 0 5px;
+}
+
+.input-group-btn {
.btn {
- padding: 9px 0px;
- font-size: 15px;
- color: #7f8fa4;
- border-color: #e7e9ed;
- width: 140px;
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
+ @include btn-gray;
+ @include btn-middle;
+
+ &:hover {
+ outline: none;
}
- &.active {
- border-color: $gl-info;
- background: $gl-info;
- color: #fff;
+ &:focus {
+ outline: none;
+ }
+
+ &:active {
+ outline: none;
+ }
- .badge {
- color: $gl-info;
- background-color: white;
- }
+ &.btn-clipboard {
+ padding-left: 15px;
+ padding-right: 15px;
}
}
-}
-.btn-clipboard {
- border: none;
+ .active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ border: 1px solid #c6cacf !important;
+ background-color: #e4e7ed !important;
+ }
+
+ .btn-green {
+ @include btn-green
+ }
}
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/common.scss b/app/assets/stylesheets/framework/common.scss
index 11730000f85..05645116268 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -374,75 +374,6 @@ table {
}
}
-.center-top-menu, .left-top-menu {
- @include nav-menu;
- text-align: center;
- margin-top: 5px;
- margin-bottom: $gl-padding;
- height: auto;
- margin-top: -$gl-padding;
-
- &.no-bottom {
- margin-bottom: 0;
- }
-
- &.no-top {
- margin-top: 0;
- }
-
- li a {
- display: inline-block;
- padding-top: $gl-padding;
- padding-bottom: 11px;
- margin-bottom: -1px;
- }
-
- &.bottom-border {
- border-bottom: 1px solid $border-color;
- height: 57px;
- }
-
- &.wide {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- }
-}
-
-.left-top-menu {
- text-align: left;
- border-bottom: 1px solid #EEE;
-}
-
-.center-middle-menu {
- @include nav-menu;
- padding: 0;
- text-align: center;
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
- height: 58px;
- border-bottom: 1px solid $border-color;
-
- li {
- &:after {
- content: "|";
- color: $border-gray-light;
- }
-
- &:last-child {
- &:after {
- content: none;
- }
- }
-
- > a {
- display: inline-block;
- text-transform: uppercase;
- font-size: 13px;
- }
- }
-}
-
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index cbfd4bc29b6..6ee104ee31a 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -3,11 +3,8 @@
*
*/
.file-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
border: none;
- border-top: 1px solid #E7E9EE;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
&.readme-holder {
border-bottom: 0;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 82eb50ad4be..1bfd0213995 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -8,10 +8,12 @@
.flash-notice {
@extend .alert;
@extend .alert-info;
+ margin: 0;
}
.flash-alert {
@extend .alert;
@extend .alert-danger;
+ margin: 0;
}
}
diff --git a/app/assets/stylesheets/framework/fonts.scss b/app/assets/stylesheets/framework/fonts.scss
index e214567eca1..20988f7b430 100644
--- a/app/assets/stylesheets/framework/fonts.scss
+++ b/app/assets/stylesheets/framework/fonts.scss
@@ -3,23 +3,23 @@
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
- src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf');
+ src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf.woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
- src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf');
+ src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf.woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
- src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf');
+ src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf.woff');
}
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 700;
- src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf');
+ src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf.woff');
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 032d343df44..4dab806d50e 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -74,8 +74,10 @@ label {
.form-control {
@include box-shadow(none);
- height: 42px;
- padding: 8px $gl-padding;
+}
+
+.form-control-inline {
+ display: inline;
}
.wiki-content {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 4dbbb56104b..ba5e72c8c5a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -28,6 +28,7 @@ header {
min-height: $header-height;
background-color: #fff;
border: none;
+ border-bottom: 1px solid #EEE;
.container-fluid {
width: 100% !important;
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index a1a9990241d..e901c78d02f 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -5,8 +5,6 @@ html {
}
body {
- background-color: #F3F3F3 !important;
-
&.navless {
background-color: white !important;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 1c74e525a60..c6bc6fb324d 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -74,7 +74,7 @@
/** light list with border-bottom between li **/
-ul.bordered-list {
+ul.bordered-list, ul.unstyled-list {
@include basic-list;
&.top-list {
@@ -88,6 +88,10 @@ ul.bordered-list {
}
}
+ul.unstyled-list > li {
+ border-bottom: none;
+}
+
ul.task-list {
li.task-list-item {
list-style-type: none;
@@ -105,10 +109,8 @@ ul.content-list {
padding: 0;
> li {
- padding: $gl-padding;
+ padding: $gl-padding 0;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
color: $gl-gray;
.avatar {
@@ -129,6 +131,7 @@ ul.content-list {
.panel > .content-list {
li {
margin: 0;
+ padding: $gl-padding;
}
}
@@ -144,7 +147,7 @@ ul.controls {
> li {
float: left;
margin-right: 10px;
-
+
&:last-child {
margin-right: 0;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 4a00a197d9a..6732343802a 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,13 +65,6 @@
position: relative;
}
-.md-header {
- ul {
- float: left;
- margin-bottom: 1px;
- }
-}
-
.referenced-users {
color: #4c4e54;
padding-top: 10px;
@@ -85,28 +78,12 @@
box-shadow: none;
}
-.new_note,
-.edit_note,
-.detail-page-description,
-.milestone-description,
-.wiki-content,
-.merge-request-form {
- .nav-tabs {
- margin-bottom: 0;
- border: none;
-
- li a,
- li.active a {
- border: 1px solid #DDD;
- }
- }
-}
-
.markdown-area {
@include border-radius(0);
background: #FFF;
border: 1px solid #ddd;
min-height: 140px;
+ max-height: 430px;
padding: 5px;
box-shadow: none;
width: 100%;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 41fd890f14f..1d5000fe388 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -118,38 +118,3 @@
font-size: 16px;
line-height: 24px;
}
-
-@mixin nav-menu {
- padding: 0;
- margin: 0;
- list-style: none;
- height: 56px;
-
- li {
- display: inline-block;
-
- a {
- padding: 14px;
- font-size: 15px;
- line-height: 28px;
- color: #959494;
- border-bottom: 2px solid transparent;
-
- &:hover, &:active, &:focus {
- text-decoration: none;
- outline: none;
- }
- }
-
- &.active a {
- color: #616060;
- border-bottom: 2px solid #4688f1;
- }
-
- .badge {
- font-weight: normal;
- background-color: #eee;
- color: #78a;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index c00709fb6bb..0997dfc287c 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -9,7 +9,7 @@
padding-right: 5px;
}
- .nav.nav-tabs > li > a {
+ .nav-links > li > a {
padding: 10px;
font-size: 12px;
margin-right: 3px;
@@ -81,7 +81,7 @@
display: none;
}
- .center-top-menu, .left-top-menu {
+ .nav-links, .nav-links {
li a {
font-size: 14px;
padding: 19px 10px;
@@ -100,11 +100,6 @@
}
@media (max-width: $screen-sm-max) {
- .page-with-sidebar .content-wrapper {
- padding: 0;
- padding-top: 1px;
- }
-
.issues-filters {
.milestone-filter, .labels-filter {
display: none;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
new file mode 100644
index 00000000000..c537d97fb24
--- /dev/null
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -0,0 +1,39 @@
+.nav-links {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ height: auto;
+ border-bottom: 1px solid $border-color;
+
+ li {
+ display: inline-block;
+
+ a {
+ display: inline-block;
+ padding: 14px;
+ padding-top: $gl-padding;
+ padding-bottom: 11px;
+ margin-bottom: -1px;
+ font-size: 15px;
+ line-height: 28px;
+ color: #959494;
+ border-bottom: 2px solid transparent;
+
+ &:hover, &:active, &:focus {
+ text-decoration: none;
+ outline: none;
+ }
+ }
+
+ &.active a {
+ color: #000000;
+ border-bottom: 2px solid #4688f1;
+ }
+
+ .badge {
+ font-weight: normal;
+ background-color: #eee;
+ color: #78a;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index af145191bc8..3ee3443e349 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -3,8 +3,8 @@
.select2-choice {
background: #FFF;
border-color: #DDD;
- height: 42px;
- padding: 8px $gl-padding;
+ height: 36px;
+ padding: 6px $gl-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 458af76cb75..540d0b03163 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -21,11 +21,10 @@
.content-wrapper {
width: 100%;
- padding: 20px;
.container-fluid {
background: #FFF;
- padding: $gl-padding;
+ padding: 0 $gl-padding;
&.container-blank {
background: none;
@@ -105,7 +104,7 @@
.tanuki-shape {
transition: all 0.8s;
- &:hover {
+ &:hover, &.highlight {
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 793ab3d9bb9..c4e9f467ce4 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -1,13 +1,11 @@
.table-holder {
- margin: -$gl-padding;
- margin-top: 0;
- margin-bottom: 0;
+ margin: 0;
}
table {
&.table {
margin-bottom: $gl-padding;
-
+
.dropdown-menu a {
text-decoration: none;
}
@@ -32,6 +30,7 @@ table {
}
th {
+ background-color: $background-color;
font-weight: normal;
font-size: 15px;
border-bottom: 1px solid $border-color !important;
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index ff41e26ed8a..47b843e5e3d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -5,10 +5,8 @@
padding: 0;
.timeline-entry {
- padding: $gl-padding;
+ padding: $gl-padding 0;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
color: $gl-gray;
border-bottom: 1px solid $border-white-light;
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 94f0ed761df..88072606bf5 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -99,47 +99,6 @@
}
}
-// Nav tabs
-.nav.nav-tabs {
- margin-bottom: 15px;
-
- li {
- > a {
- margin-right: 5px;
- line-height: 20px;
- border-color: #EEE;
- color: #888;
- border-bottom: 1px solid #ddd;
- .badge {
- background-color: #eee;
- color: #888;
- text-shadow: 0 1px 1px #fff;
- }
- i.fa {
- line-height: 14px;
- }
- }
- &.active {
- > a {
- border-color: #CCC;
- border-bottom: 1px solid #fff;
- color: #333;
- font-weight: bold;
- }
- }
- }
-}
-
-.nav-tabs > li > a,
-.nav-pills > li > a {
- color: #666;
-}
-
-.nav-pills > .active > a > span > .badge {
- background-color: #fff;
- color: $gl-primary;
-}
-
/**
* fix to keep tooltips position in top navigation bar
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 63868a34e2a..cd0621cdbf3 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -46,7 +46,7 @@ $font-size-base: $gl-font-size;
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$padding-base-vertical: 9px;
+$padding-base-vertical: $gl-vert-padding;
$padding-base-horizontal: $gl-padding;
$component-active-color: #fff;
$component-active-bg: $brand-info;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c3e4ad0ad00..ab4f71af039 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 {
@@ -177,7 +177,7 @@ body {
}
.page-title {
- margin-top: 0px;
+ margin-top: $gl-padding;
line-height: 1.3;
font-size: 1.25em;
font-weight: 600;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index af75123b0af..85ecdddda79 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -22,8 +22,10 @@ $header-height: 58px;
$fixed-layout-width: 1280px;
$gl-gray: #5a5a5a;
$gl-padding: 16px;
+$gl-vert-padding: 6px;
$gl-padding-top:10px;
$gl-avatar-size: 46px;
+$secondary-text: #7f8fa4;
/*
* Color schema
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 32e2c020e06..c3f27333fad 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -1,17 +1,13 @@
.zennable {
- .zen-toggle-comment {
- display: none;
- }
-
- .zen-enter-link {
+ a.js-zen-enter {
color: $gl-gray;
position: absolute;
top: 0px;
right: 4px;
- line-height: 40px;
+ line-height: 56px;
}
- .zen-leave-link {
+ a.js-zen-leave {
display: none;
color: $gl-text-color;
position: absolute;
@@ -25,62 +21,41 @@
}
}
- // Hide the Enter link when we're in Zen mode
- input:checked ~ .zen-backdrop .zen-enter-link {
- display: none;
- }
-
- // Show the Leave link when we're in Zen mode
- input:checked ~ .zen-backdrop .zen-leave-link {
- display: block;
- position: absolute;
- top: 0;
- }
-
- input:checked ~ .zen-backdrop {
- background-color: white;
- position: fixed;
- top: 0;
- bottom: 0;
- left: 0;
- right: 0;
- z-index: 1031;
-
- textarea {
- border: none;
- box-shadow: none;
- border-radius: 0;
- color: #000;
- font-size: 20px;
- line-height: 26px;
- padding: 30px;
- display: block;
- outline: none;
- resize: none;
- height: 100vh;
- max-width: 900px;
- margin: 0 auto;
+ .zen-backdrop {
+ &.fullscreen {
+ background-color: white;
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ z-index: 1031;
+
+ textarea {
+ border: none;
+ box-shadow: none;
+ border-radius: 0;
+ color: #000;
+ font-size: 20px;
+ line-height: 26px;
+ padding: 30px;
+ display: block;
+ outline: none;
+ resize: none;
+ height: 100vh;
+ max-width: 900px;
+ margin: 0 auto;
+ }
+
+ a.js-zen-enter {
+ display: none;
+ }
+
+ a.js-zen-leave {
+ display: block;
+ position: absolute;
+ top: 0;
+ }
}
}
-
- // Make the color of the placeholder text in the Zenned-out textarea darker,
- // so it becomes visible
-
- input:checked ~ .zen-backdrop textarea::-webkit-input-placeholder {
- color: #A8A8A8;
- }
-
- input:checked ~ .zen-backdrop textarea:-moz-placeholder {
- color: #A8A8A8;
- opacity: 1;
- }
-
- input:checked ~ .zen-backdrop textarea::-moz-placeholder {
- color: #A8A8A8;
- opacity: 1;
- }
-
- input:checked ~ .zen-backdrop textarea:-ms-input-placeholder {
- color: #A8A8A8;
- }
}
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
new file mode 100644
index 00000000000..abae5c3d0a5
--- /dev/null
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -0,0 +1,3 @@
+.branch-name{
+ font-weight: 600;
+}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 17245d3be7b..6ec88bdd804 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -2,6 +2,10 @@
display: block;
}
+.commit-row-title .commit-title {
+ font-weight: 600;
+}
+
.commit-author, .commit-committer{
display: block;
color: #999;
@@ -35,6 +39,8 @@
}
.commit-box {
+ border-top: 1px solid $border-color;
+
.commit-title {
margin: 0;
font-size: 23px;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c9dfcff6290..800df95cff3 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -28,10 +28,6 @@
}
}
-.commits-feed-holder {
- float: right;
-}
-
li.commit {
list-style: none;
@@ -122,3 +118,59 @@ li.commit {
color: $gl-gray;
}
}
+
+.divergence-graph {
+ padding: 12px 12px 0 0;
+ float: right;
+
+ .graph-side {
+ position: relative;
+ width: 80px;
+ height: 22px;
+ padding: 5px 0 13px;
+ float: left;
+
+ .bar {
+ position: absolute;
+ height: 4px;
+ background-color: #ccc;
+ }
+
+ .bar-behind {
+ right: 0;
+ border-radius: 3px 0 0 3px;
+ }
+
+ .bar-ahead {
+ left: 0;
+ border-radius: 0 3px 3px 0;
+ }
+
+ .count {
+ padding-top: 6px;
+ padding-bottom: 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
index deab805dbc2..529a43548c8 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -1,7 +1,5 @@
.detail-page-header {
- margin: -$gl-padding;
- padding: 7px $gl-padding;
- margin-bottom: 0px;
+ padding: 11px 0;
border-bottom: 1px solid $border-color;
color: #5c5d5e;
font-size: 16px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index caaad1e31d3..012232a708e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,9 +1,7 @@
// Common
.diff-file {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- border: none;
- border-bottom: 1px solid #E7E9EE;
+ border: 1px solid $border-color;
+ border-top: none;
.diff-header {
position: relative;
@@ -23,14 +21,6 @@
}
}
- .diff-controls {
- .btn {
- padding: 0px 10px;
- font-size: 13px;
- line-height: 28px;
- }
- }
-
.commit-short-id {
font-family: $monospace_font;
font-size: smaller;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 282aaf2219b..8fa15b35748 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,9 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding $gl-padding $gl-padding ($gl-padding + $gl-avatar-size + 15px);
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
+ padding: $gl-padding 0 $gl-padding ($gl-avatar-size + 15px);
border-bottom: 1px solid $table-border-color;
color: #7f8fa4;
@@ -138,6 +136,7 @@
*/
.event-last-push {
overflow: auto;
+ width: 100%;
.event-last-push-text {
@include str-truncated(100%);
padding: 5px 0;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 263993f59a5..3404c2631e1 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -11,3 +11,8 @@
height: 42px;
}
}
+
+.content-list .group-name {
+ font-weight: 600;
+ color: #4c4e54;
+}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 9da273a0b6b..eae3590a189 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -27,10 +27,10 @@
.project-issuable-filter {
.controls {
float: right;
- margin-top: 7px;
+ margin-top: 11px;
}
- .center-top-menu {
+ .nav-links {
text-align: left;
}
}
@@ -94,11 +94,24 @@
}
.cross-project-reference {
- font-weight: bold;
color: $gl-link-color;
+ span {
+ white-space: nowrap;
+ width: 85%;
+ overflow: hidden;
+ position: relative;
+ display: inline-block;
+ text-overflow: ellipsis;
+ }
+
+ cite {
+ font-style: normal;
+ }
+
button {
float: right;
+ padding: 3px 5px;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index a02a3a72e79..ad92cc22815 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -6,7 +6,7 @@
.issue-title {
margin-bottom: 5px;
font-size: $list-font-size;
- font-weight: bold;
+ font-weight: 600;
}
.issue-info {
@@ -144,3 +144,8 @@ form.edit-issue {
.issue-form .select2-container {
width: 250px !important;
}
+
+.issue-closed-by-widget {
+ color: $secondary-text;
+ margin-left: 52px;
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 82effde0bf3..75f2ae80a92 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,9 +3,9 @@
*
*/
.mr-state-widget {
- background: #F7F8FA;
+ background: $background-color;
color: $gl-gray;
- border: 1px solid #dce0e6;
+ border: 1px solid $border-color;
@include border-radius(2px);
form {
@@ -150,7 +150,7 @@
.merge-request-title {
margin-bottom: 5px;
font-size: $list-font-size;
- font-weight: bold;
+ font-weight: 600;
}
.merge-request-info {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index d86259f93fb..2c9a42f9892 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -159,6 +159,7 @@
.edit_note {
.markdown-area {
min-height: 140px;
+ max-height: 430px;
}
.note-form-actions {
background: transparent;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cff3edb7ed2..13b0ed769fc 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -26,6 +26,15 @@
}
.project-home-panel {
+ padding-bottom: 40px;
+ border-bottom: 1px solid $border-color;
+
+ .cover-controls {
+ .project-settings-dropdown {
+ margin-left: 10px;
+ }
+ }
+
.project-identicon-holder {
margin-bottom: 16px;
@@ -44,6 +53,8 @@
}
.notifications-btn {
+ margin-top: -28px;
+
.fa-bell {
margin-right: 6px;
}
@@ -68,17 +79,6 @@
}
}
- .git-clone-holder {
- max-width: 498px;
-
- .form-control {
- background: #FFF;
- font-size: 14px;
- height: 42px;
- margin-left: -1px;
- }
- }
-
.visibility-level-label {
@extend .btn;
@extend .btn-gray;
@@ -91,11 +91,6 @@
}
}
- .git-clone-holder {
- display: inline-table;
- position: relative;
- }
-
.project-repo-buttons {
margin-top: 12px;
margin-bottom: 0px;
@@ -105,10 +100,22 @@
margin-bottom: 12px;
}
+ .clone-row {
+ .split-repo-buttons,
+ .project-clone-holder {
+ display: inline-block;
+ }
+
+ .split-repo-buttons {
+ margin: 0 12px;
+ }
+ }
+
.btn {
@include btn-gray;
text-transform: none;
}
+
.count-with-arrow {
display: inline-block;
position: relative;
@@ -153,8 +160,8 @@
border-style: solid;
font-size: 13px;
font-weight: 600;
- line-height: 20px;
- padding: 11px 16px;
+ line-height: 13px;
+ padding: $gl-vert-padding $gl-padding;
letter-spacing: .4px;
padding: 10px;
text-align: center;
@@ -182,118 +189,6 @@
}
}
-.git-clone-holder {
- .project-home-dropdown + & {
- margin-right: 45px;
- }
-
- .clone-options {
- display: table-cell;
- a.btn {
- width: 100%;
- }
- }
-
- .form-control {
- cursor: auto;
- @extend .monospace;
- background: #FAFAFA;
- width: 101%;
- }
-
- .input-group-addon {
- background: #f7f8fa;
-
- &.git-protocols {
- padding: 0;
- border: none;
-
- .input-group-btn:last-child > .btn {
- @include border-radius-right(0);
-
- border-left: 1px solid #c6cacf;
- margin-left: -2px !important;
- }
- }
- }
-}
-
-.projects-search-form {
-
- .input-group .form-control {
- height: 42px;
- }
-}
-
-.input-group-btn {
- .btn {
- @include btn-gray;
- @include btn-middle;
-
- &:hover {
- outline: none;
- }
-
- &:focus {
- outline: none;
- }
-
- &:active {
- outline: none;
- }
-
- &.btn-clipboard {
- padding-left: 15px;
- padding-right: 15px;
- }
- }
-
- .active {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- }
-
- .btn-green {
- @include btn-green
- }
-
-}
-
-.split-repo-buttons {
- display: inline-table;
- margin: 0 12px 0 12px;
-
- .btn{
- @include btn-gray;
- @include btn-default;
- }
-
- .dropdown-toggle {
- margin: -5px;
- }
-}
-
-#notification-form {
- margin-left: 5px;
-}
-
-.dropdown-new {
- margin-left: -5px;
-}
-
-.open > .dropdown-new.btn {
- @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
-
- border: 1px solid #c6cacf !important;
- background-color: #e4e7ed !important;
- text-transform: uppercase;
- color: #313236 !important;
- font-size: 13px;
- font-weight: 600;
-}
-
.dropdown-menu {
@include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
@include border-radius (0px);
@@ -345,28 +240,6 @@
color: #555;
}
-ul.nav.nav-projects-tabs {
- @extend .nav-tabs;
-
- padding-left: 8px;
-
- li {
- a {
- padding: 6px 25px;
- margin-top: 2px;
- border-color: #DDD;
- background-color: #EEE;
- text-shadow: 0 1px 1px white;
- color: #555;
- }
- &.active {
- a {
- font-weight: bold;
- }
- }
- }
-}
-
.project_member_row form {
margin: 0px;
}
@@ -393,9 +266,9 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb {
padding: 0;
- line-height: 42px;
background: transparent;
border: none;
+ line-height: 42px;
margin: 0;
> li + li:before {
@@ -404,12 +277,14 @@ ul.nav.nav-projects-tabs {
}
}
+.last-push-widget {
+ margin-top: -1px;
+}
+
.top-area {
border-bottom: 1px solid #EEE;
- margin: 0 -16px;
- padding: 0 $gl-padding;
- ul.left-top-menu {
+ ul.nav-links {
display: inline-block;
width: 50%;
margin-bottom: 0px;
@@ -420,12 +295,12 @@ ul.nav.nav-projects-tabs {
width: 50%;
display: inline-block;
float: right;
- padding-top: 7px;
+ padding-top: 11px;
text-align: right;
.btn-green {
- margin-top: -2px;
margin-left: 10px;
+ float: right;
}
}
@@ -471,11 +346,11 @@ table.table.protected-branches-list tr.no-border {
padding-top: 10px;
padding-bottom: 4px;
- ul.nav-pills {
+ ul.nav {
display:inline-block;
}
- .nav-pills li {
+ .nav li {
display:inline;
}
@@ -512,8 +387,8 @@ pre.light-well {
}
.projects-search-form {
- margin: -$gl-padding;
- padding: $gl-padding;
+ padding: $gl-padding 0;
+ padding-bottom: 0;
margin-bottom: 0px;
input {
@@ -562,10 +437,8 @@ pre.light-well {
@include basic-list;
.project-row {
- padding: $gl-padding;
+ padding: $gl-padding 0;
border-color: $table-border-color;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
&.no-description {
.project {
@@ -619,8 +492,6 @@ pre.light-well {
}
.project-last-commit {
- margin: 0 7px;
-
.ci-status {
margin-right: 16px;
}
@@ -650,9 +521,7 @@ pre.light-well {
}
.project-show-readme .readme-holder {
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
- padding: ($gl-padding + 7px);
+ padding: $gl-padding 0;
border-top: 0;
.edit-project-readme {
@@ -660,3 +529,32 @@ pre.light-well {
position: relative;
}
}
+
+.git-clone-holder {
+ width: 498px;
+
+ .btn-clipboard {
+ border: 1px solid $border-color;
+ padding: 6px $gl-padding;
+ }
+
+ .project-home-dropdown + & {
+ margin-right: 45px;
+ }
+
+ .clone-options {
+ display: table-cell;
+ a.btn {
+ width: 100%;
+ }
+ }
+
+ .form-control {
+ @extend .monospace;
+ background: #FFF;
+ font-size: 14px;
+ margin-left: -1px;
+ cursor: auto;
+ width: 101%;
+ }
+}
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
new file mode 100644
index 00000000000..e9cd6dc6c5e
--- /dev/null
+++ b/app/assets/stylesheets/pages/tags.scss
@@ -0,0 +1,3 @@
+.tag-name{
+ font-weight: 600;
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index d4ab6967ccd..6a6dd7dfc85 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -1,11 +1,22 @@
.tree-holder {
+ > .nav-block {
+ margin: 11px 0;
+ }
+
+ .file-finder {
+ width: 50%;
+ .file-finder-input {
+ width: 95%;
+ display: inline-block;
+ }
+ }
.tree-table {
margin-bottom: 0;
tr {
> td, > th {
- line-height: 28px;
+ line-height: 26px;
}
&:hover {
@@ -78,12 +89,14 @@
.blob-commit-info {
list-style: none;
+ padding: $gl-padding;
+ background: $background-color;
+ border: 1px solid $border-color;
+ border-bottom: none;
margin: 0;
- padding: 0;
- margin-bottom: 5px;
.commit {
- padding: $gl-padding 0;
+ padding: 0;
.commit-row-title {
.commit-row-message {
@@ -107,3 +120,8 @@
font-weight: normal;
color: $md-link-color;
}
+
+.tree-controls {
+ float: right;
+ margin-top: 11px;
+}
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 20bc5173f1d..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.notify(@abuse_report.id).deliver_later
- end
+ @abuse_report.notify
message = "Thank you for your report. A GitLab administrator will look into it shortly."
- redirect_to root_path, notice: message
+ redirect_to @abuse_report.user, notice: message
else
render :new
end
@@ -23,6 +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/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 38a5a9fca08..2463cfa87be 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -6,11 +6,9 @@ class Admin::AbuseReportsController < Admin::ApplicationController
def destroy
abuse_report = AbuseReport.find(params[:id])
- if params[:remove_user]
- abuse_report.user.destroy
- end
-
+ abuse_report.remove_user if params[:remove_user]
abuse_report.destroy
+
render nothing: true
end
end
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 3c332adf1fa..91f7d78bd73 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -69,12 +69,14 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:max_artifacts_size,
:metrics_enabled,
:metrics_host,
- :metrics_database,
- :metrics_username,
- :metrics_password,
+ :metrics_port,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold,
+ :metrics_sample_interval,
+ :recaptcha_enabled,
+ :recaptcha_site_key,
+ :recaptcha_private_key,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 497c34f8f49..4735b27c65d 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,8 +1,12 @@
class Admin::BroadcastMessagesController < Admin::ApplicationController
- before_action :broadcast_messages
+ before_action :finder, only: [:edit, :update, :destroy]
def index
- @broadcast_message = BroadcastMessage.new
+ @broadcast_messages = BroadcastMessage.reorder("starts_at ASC").page(params[:page])
+ @broadcast_message = BroadcastMessage.new
+ end
+
+ def edit
end
def create
@@ -15,8 +19,16 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
end
+ def update
+ if @broadcast_message.update(broadcast_message_params)
+ redirect_to admin_broadcast_messages_path, notice: 'Broadcast Message was successfully updated.'
+ else
+ render :edit
+ end
+ end
+
def destroy
- BroadcastMessage.find(params[:id]).destroy
+ @broadcast_message.destroy
respond_to do |format|
format.html { redirect_back_or_default(default: { action: 'index' }) }
@@ -26,14 +38,17 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
protected
- def broadcast_messages
- @broadcast_messages ||= BroadcastMessage.order("starts_at DESC").page(params[:page])
+ def finder
+ @broadcast_message = BroadcastMessage.find(params[:id])
end
def broadcast_message_params
- params.require(:broadcast_message).permit(
- :alert_type, :color, :ends_at, :font,
- :message, :starts_at
- )
+ params.require(:broadcast_message).permit(%i(
+ color
+ ends_at
+ font
+ message
+ starts_at
+ ))
end
end
diff --git a/app/controllers/admin/builds_controller.rb b/app/controllers/admin/builds_controller.rb
index 83d9684c706..0db91eaaf2e 100644
--- a/app/controllers/admin/builds_controller.rb
+++ b/app/controllers/admin/builds_controller.rb
@@ -5,12 +5,12 @@ class Admin::BuildsController < Admin::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb
index e383fe38ea6..79a53556f0a 100644
--- a/app/controllers/admin/identities_controller.rb
+++ b/app/controllers/admin/identities_controller.rb
@@ -26,6 +26,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def update
if @identity.update_attributes(identity_params)
+ RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.'
else
render :edit
@@ -34,6 +35,7 @@ class Admin::IdentitiesController < Admin::ApplicationController
def destroy
if @identity.destroy
+ RepairLdapBlockedUserService.new(@user).execute
redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully removed.'
else
redirect_to admin_user_identities_path(@user), alert: 'Failed to remove user identity.'
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index d7c927d444c..87f4fb455b8 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -40,7 +40,9 @@ class Admin::UsersController < Admin::ApplicationController
end
def unblock
- if user.activate
+ if user.ldap_blocked?
+ redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab")
+ elsif user.activate
redirect_back_or_admin_user(notice: "Successfully unblocked")
else
redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked")
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index d3c1ff035f5..8484a502024 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -287,7 +287,7 @@ class ApplicationController < ActionController::Base
end
def set_filters_params
- params[:sort] ||= 'created_desc'
+ params[:sort] ||= 'id_desc'
params[:scope] = 'all' if params[:scope].blank?
params[:state] = 'opened' if params[:state].blank?
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e782a51e7eb..a7af3cb8345 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -6,11 +6,13 @@ module Ci
end
def create
- if params[:content].blank?
+ @content = params[:content]
+
+ if @content.blank?
@status = false
@error = "Please provide content of .gitlab-ci.yml"
else
- @config_processor = Ci::GitlabCiYamlProcessor.new params[:content]
+ @config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@status = true
diff --git a/app/controllers/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/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
new file mode 100644
index 00000000000..dff0732bdfe
--- /dev/null
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -0,0 +1,56 @@
+class Projects::ArtifactsController < Projects::ApplicationController
+ layout 'project'
+ before_action :authorize_read_build_artifacts!
+
+ 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 browse
+ return render_404 unless build.artifacts?
+
+ directory = params[:path] ? "#{params[:path]}/" : ''
+ @entry = build.artifacts_metadata_entry(directory)
+
+ return render_404 unless @entry.exists?
+ end
+
+ def file
+ entry = build.artifacts_metadata_entry(params[:path])
+
+ if entry.exists?
+ render json: { archive: build.artifacts_file.path,
+ entry: Base64.encode64(entry.path) }
+ else
+ render json: {}, status: 404
+ end
+ end
+
+ private
+
+ def build
+ @build ||= project.builds.unscoped.find_by!(id: params[:build_id])
+ end
+
+ def artifacts_file
+ @artifacts_file ||= build.artifacts_file
+ end
+
+ def authorize_read_build_artifacts!
+ unless can?(current_user, :read_build_artifacts, @project)
+ if current_user.nil?
+ return authenticate_user!
+ else
+ return render_404
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 3c2849a7601..4db3b3bf23d 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -9,6 +9,11 @@ class Projects::BranchesController < Projects::ApplicationController
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
@branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
+
+ @max_commits = @branches.reduce(0) do |memo, branch|
+ diverging_commit_counts = repository.diverging_commit_counts(branch)
+ [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
+ end
end
def recent
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 26ba12520c7..0e965966ffa 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -2,7 +2,6 @@ class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_manage_builds!, except: [:index, :show, :status]
- before_action :authorize_download_build_artifacts!, only: [:download]
layout "project"
@@ -12,12 +11,12 @@ class Projects::BuildsController < Projects::ApplicationController
@builds = @all_builds.order('created_at DESC')
@builds =
case @scope
- when 'all'
- @builds
+ when 'running'
+ @builds.running_or_pending.reverse_order
when 'finished'
@builds.finished
else
- @builds.running_or_pending.reverse_order
+ @builds
end
@builds = @builds.page(params[:page]).per(30)
end
@@ -51,18 +50,6 @@ class Projects::BuildsController < Projects::ApplicationController
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
render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
end
@@ -79,10 +66,6 @@ class Projects::BuildsController < Projects::ApplicationController
@build ||= project.builds.unscoped.find_by!(id: params[:id])
end
- def artifacts_file
- build.artifacts_file
- end
-
def build_path(build)
namespace_project_build_path(build.project.namespace, build.project, build)
end
@@ -92,14 +75,4 @@ class Projects::BuildsController < Projects::ApplicationController
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/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 04a88990bf4..bf5b54c8cb7 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -8,10 +8,16 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :authorize_download_code!
def show
- @repo = @project.repository
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
+ search = params[:search]
+
+ @commits =
+ if search.present?
+ @repository.find_commits_by_message(search, @ref, @path, @limit, @offset).compact
+ else
+ @repository.commits(@ref, @path, @limit, @offset)
+ end
- @commits = @repo.commits(@ref, @path, @limit, @offset)
@note_counts = project.notes.where(commit_id: @commits.map(&:id)).
group(:commit_id).count
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
new file mode 100644
index 00000000000..54a0c447aee
--- /dev/null
+++ b/app/controllers/projects/find_file_controller.rb
@@ -0,0 +1,26 @@
+# Controller for viewing a repository's file structure
+class Projects::FindFileController < Projects::ApplicationController
+ include ExtractsPath
+ include ActionView::Helpers::SanitizeHelper
+ include TreeHelper
+
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
+
+ def show
+ return render_404 unless @repository.commit(@ref)
+
+ respond_to do |format|
+ format.html
+ end
+ end
+
+ def list
+ file_paths = @repo.ls_files(@ref)
+
+ respond_to do |format|
+ format.json { render json: file_paths }
+ end
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index b59b52291fb..f476afb2d92 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -61,7 +61,7 @@ class Projects::IssuesController < Projects::ApplicationController
@note = @project.notes.new(noteable: @issue)
@notes = @issue.notes.nonawards.with_associations.fresh
@noteable = @issue
- @merge_requests = @issue.referenced_merge_requests
+ @merge_requests = @issue.referenced_merge_requests(current_user)
respond_with(@issue)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index ab5c953189c..de948d271c8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -153,7 +153,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_check
- @merge_request.check_if_can_be_merged if @merge_request.unchecked?
+ @merge_request.check_if_can_be_merged
render partial: "projects/merge_requests/widget/show.html.haml", layout: false
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index c4e18c17077..a8f091819ca 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -20,6 +20,8 @@ class Projects::RefsController < Projects::ApplicationController
namespace_project_network_path(@project.namespace, @project, @id, @options)
when "graphs"
namespace_project_graph_path(@project.namespace, @project, @id)
+ when "find_file"
+ namespace_project_find_file_path(@project.namespace, @project, @id)
when "graphs_commits"
commits_namespace_project_graph_path(@project.namespace, @project, @id)
else
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 3004722bce0..935f7d75c6a 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -8,7 +8,7 @@ class ProjectsController < ApplicationController
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
# Authorize
- before_action :authorize_admin_project!, only: [:edit, :update]
+ before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping]
before_action :event_filter, only: [:show, :activity]
layout :determine_layout
@@ -166,6 +166,15 @@ class ProjectsController < ApplicationController
end
end
+ def housekeeping
+ ::Projects::HousekeepingService.new(@project).execute
+
+ respond_to do |format|
+ flash[:notice] = "Housekeeping successfully started."
+ format.html { redirect_to project_path(@project) }
+ end
+ end
+
def toggle_star
current_user.toggle_star(@project)
@project.reload
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index ee1006dea49..c48175a4c5a 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -7,7 +7,7 @@ class RegistrationsController < Devise::RegistrationsController
end
def create
- if !Gitlab.config.recaptcha.enabled || verify_recaptcha
+ 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."
diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb
new file mode 100644
index 00000000000..7271c933b9b
--- /dev/null
+++ b/app/controllers/sent_notifications_controller.rb
@@ -0,0 +1,25 @@
+class SentNotificationsController < ApplicationController
+ skip_before_action :authenticate_user!
+
+ def unsubscribe
+ @sent_notification = SentNotification.for(params[:id])
+ return render_404 unless @sent_notification && @sent_notification.unsubscribable?
+
+ noteable = @sent_notification.noteable
+ noteable.unsubscribe(@sent_notification.recipient)
+
+ flash[:notice] = "You have been unsubscribed from this thread."
+ if current_user
+ case noteable
+ when Issue
+ redirect_to issue_path(noteable)
+ when MergeRequest
+ redirect_to merge_request_path(noteable)
+ else
+ redirect_to root_path
+ end
+ else
+ redirect_to new_user_session_path
+ end
+ end
+end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index da4b35d322b..825f85199be 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -5,6 +5,7 @@ class SessionsController < Devise::SessionsController
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
@@ -108,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/users_controller.rb b/app/controllers/users_controller.rb
index 30cb869eb2a..280228dbcc0 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -7,7 +7,7 @@ class UsersController < ApplicationController
@projects = PersonalProjectsFinder.new(@user).execute(current_user)
- @groups = JoinedGroupsFinder.new(@user).execute(current_user)
+ @groups = @user.groups.order_id_desc
respond_to do |format|
format.html
diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb
deleted file mode 100644
index 91cb0f228f0..00000000000
--- a/app/finders/groups_finder.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-class GroupsFinder
- # Finds the groups available to the given user.
- #
- # current_user - The user to find the groups for.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # This method returns the groups "current_user" can see.
- def groups_visible_to_user(current_user)
- base = groups_for_projects(public_and_internal_projects)
-
- union = Gitlab::SQL::Union.
- new([base.select(:id), current_user.authorized_groups.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(public_projects)
- end
-
- def groups_for_projects(projects)
- Group.public_and_given_groups(projects.select(:namespace_id))
- end
-
- def public_projects
- Project.unscoped.public_only
- end
-
- def public_and_internal_projects
- Project.unscoped.public_and_internal_only
- end
-end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 3d5e8b6fbe7..4d56b48e3f8 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -79,9 +79,9 @@ class IssuableFinder
if project?
@projects = project
elsif current_user && params[:authorized_only].presence && !current_user_related?
- @projects = current_user.authorized_projects
+ @projects = current_user.authorized_projects.reorder(nil)
else
- @projects = ProjectsFinder.new.execute(current_user)
+ @projects = ProjectsFinder.new.execute(current_user).reorder(nil)
end
end
diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb
deleted file mode 100644
index e7523136fea..00000000000
--- a/app/finders/joined_groups_finder.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-# Class for finding the groups a user is a member of.
-class JoinedGroupsFinder
- def initialize(user = nil)
- @user = user
- end
-
- # Finds the groups of the source user, optionally limited to those visible to
- # the current user.
- #
- # current_user - If given the groups of "@user" will only include the groups
- # "current_user" can also see.
- #
- # Returns an ActiveRecord::Relation.
- def execute(current_user = nil)
- if current_user
- relation = groups_visible_to_user(current_user)
- else
- relation = public_groups
- end
-
- relation.order_id_desc
- end
-
- private
-
- # Returns the groups the user in "current_user" can see.
- #
- # This list includes all public/internal projects as well as the projects of
- # "@user" that "current_user" also has access to.
- def groups_visible_to_user(current_user)
- base = @user.authorized_groups.visible_to_user(current_user)
- extra = public_and_internal_groups
- union = Gitlab::SQL::Union.new([base.select(:id), extra.select(:id)])
-
- Group.where("namespaces.id IN (#{union.to_sql})")
- end
-
- def public_groups
- groups_for_projects(@user.authorized_projects.public_only)
- end
-
- def public_and_internal_groups
- groups_for_projects(@user.authorized_projects.public_and_internal_only)
- end
-
- def groups_for_projects(projects)
- @user.groups.public_and_given_groups(projects.select(:namespace_id))
- end
-end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0b00b9a0702..f3a2723ee0d 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -72,7 +72,7 @@ module ApplicationHelper
if user_or_email.is_a?(User)
user = user_or_email
else
- user = User.find_by(email: user_or_email)
+ user = User.find_by(email: user_or_email.downcase)
end
if user
@@ -181,10 +181,6 @@ module ApplicationHelper
end
end
- def broadcast_message
- BroadcastMessage.current
- end
-
# Render a `time` element with Javascript-based relative date and tooltip
#
# time - Time object
@@ -205,8 +201,8 @@ module ApplicationHelper
def time_ago_with_tooltip(time, placement: 'top', html_class: 'time_ago', skip_js: false)
element = content_tag :time, time.to_s,
class: "#{html_class} js-timeago js-timeago-pending",
- datetime: time.getutc.iso8601,
- title: time.in_time_zone.stamp('Aug 21, 2011 9:23pm'),
+ datetime: time.to_time.getutc.iso8601,
+ title: time.in_time_zone.to_s(:medium),
data: { toggle: 'tooltip', placement: placement, container: 'body' }
unless skip_js
@@ -266,7 +262,7 @@ module ApplicationHelper
state: params[:state],
scope: params[:scope],
label_name: params[:label_name],
- milestone_id: params[:milestone_id],
+ milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
sort: params[:sort],
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 0cfc0565e84..de669e529a7 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 facebook).freeze
+ PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2).freeze
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 6484dca6b55..1ed8c710f77 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -1,16 +1,34 @@
module BroadcastMessagesHelper
- def broadcast_styling(broadcast_message)
- styling = ''
+ def broadcast_message(message = BroadcastMessage.current)
+ return unless message.present?
+
+ content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
+ icon('bullhorn') << ' ' << message.message
+ end
+ end
+
+ def broadcast_message_style(broadcast_message)
+ style = ''
if broadcast_message.color.present?
- styling << "background-color: #{broadcast_message.color}"
- styling << '; ' if broadcast_message.font.present?
+ style << "background-color: #{broadcast_message.color}"
+ style << '; ' if broadcast_message.font.present?
end
if broadcast_message.font.present?
- styling << "color: #{broadcast_message.font}"
+ style << "color: #{broadcast_message.font}"
end
- styling
+ style
+ end
+
+ def broadcast_message_status(broadcast_message)
+ if broadcast_message.active?
+ 'Active'
+ elsif broadcast_message.ended?
+ 'Expired'
+ else
+ 'Pending'
+ end
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index ec0e3f409c1..d6c05843743 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -17,7 +17,7 @@ module ButtonHelper
def clipboard_button(data = {})
content_tag :button,
icon('clipboard'),
- class: 'btn btn-xs btn-clipboard',
+ class: 'btn btn-clipboard',
data: data,
type: :button
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index dde83ff36b5..31bf45baeb7 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -27,13 +27,15 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: "event-filter-link btn btn-default #{active}",
+ class: "event-filter-link",
id: "#{key}_event_filter",
title: "Filter by #{tooltip.downcase}",
}
- link_to request.path, link_opts do
- content_tag(:span, ' ' + tooltip)
+ content_tag :li, class: active do
+ link_to request.path, link_opts do
+ content_tag(:span, ' ' + tooltip)
+ end
end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index ca41657cec1..1a226252251 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -91,7 +91,7 @@ module GitlabMarkdownHelper
def render_wiki_content(wiki_page)
case wiki_page.format
when :markdown
- markdown(wiki_page.content)
+ markdown(wiki_page.content, pipeline: :wiki, project_wiki: @project_wiki)
when :asciidoc
asciidoc(wiki_page.content)
else
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 80e2741b09a..43262d579e9 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -80,7 +80,7 @@ module IssuesHelper
xml.link href: namespace_project_issue_url(issue.project.namespace,
issue.project, issue)
xml.title truncate(issue.title, length: 80)
- xml.updated issue.created_at.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.updated issue.created_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(issue.author_email))
xml.author do |author|
xml.name issue.author_name
@@ -99,13 +99,16 @@ module IssuesHelper
end
def emoji_icon(name, unicode = nil, aliases = [])
- unicode ||= Emoji.emoji_filename(name)
+ unicode ||= Emoji.emoji_filename(name) rescue ""
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
- "data-emoji" => name,
- "data-aliases" => aliases.join(" "),
- "data-unicode-name" => unicode
+ title: name,
+ data: {
+ aliases: aliases.join(' '),
+ emoji: name,
+ unicode_name: unicode
+ }
end
def emoji_author_list(notes, current_user)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 5f0c921413a..53c543c28c5 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -67,7 +67,7 @@ module NotesHelper
line_type: line_type
}
- button_tag class: 'btn reply-btn js-discussion-reply-button',
+ button_tag class: 'btn btn-nr reply-btn js-discussion-reply-button',
data: data, title: 'Add a reply' do
link_text = icon('comment')
link_text << ' Reply'
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 791cb9e50bd..82f805fa444 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -27,35 +27,20 @@ module PageLayoutHelper
#
# Returns an HTML-safe String.
def page_description(description = nil)
- @page_description ||= page_description_default
-
if description.present?
@page_description = description.squish
- else
+ elsif @page_description.present?
sanitize(@page_description, tags: []).truncate_words(30)
end
end
- # Default value for page_description when one hasn't been defined manually by
- # a view
- def page_description_default
- if @project
- @project.description || brand_title
- else
- brand_title
- end
- end
-
def page_image
default = image_url('gitlab_logo.png')
- if @project
- @project.avatar_url || default
- elsif @user
- avatar_icon(@user)
- else
- default
- end
+ subject = @project || @user || @group
+
+ image = subject.avatar_url if subject.present?
+ image || default
end
# Define or get attributes to be used as Twitter card metadata
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index a6ee6880247..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)
- GroupsFinder.new.execute(current_user).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/sorting_helper.rb b/app/helpers/sorting_helper.rb
index bb12d43f397..241179b0212 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -19,7 +19,7 @@ module SortingHelper
end
def sort_title_recently_updated
- 'Recently updated'
+ 'Last updated'
end
def sort_title_oldest_created
@@ -27,7 +27,7 @@ module SortingHelper
end
def sort_title_recently_created
- 'Recently created'
+ 'Last created'
end
def sort_title_milestone_soon
@@ -63,11 +63,11 @@ module SortingHelper
end
def sort_value_oldest_created
- 'created_asc'
+ 'id_asc'
end
def sort_value_recently_created
- 'created_desc'
+ 'id_desc'
end
def sort_value_milestone_soon
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/emails/issues.rb b/app/mailers/emails/issues.rb
index abdeefed5ef..4a88cb61132 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,31 +1,31 @@
module Emails
module Issues
def new_issue_email(recipient_id, issue_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ mail_new_thread(@issue, issue_thread_options(@issue.author_id, recipient_id))
end
def reassigned_issue_email(recipient_id, issue_id, previous_assignee_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def closed_issue_email(recipient_id, issue_id, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id)
- issue_mail_with_notification(issue_id, recipient_id) do
- @issue_status = status
- @updated_by = User.find updated_by_user_id
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
- end
+ setup_issue_mail(issue_id, recipient_id)
+
+ @issue_status = status
+ @updated_by = User.find updated_by_user_id
+ mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id))
end
private
@@ -38,14 +38,12 @@ module Emails
}
end
- def issue_mail_with_notification(issue_id, recipient_id)
+ def setup_issue_mail(issue_id, recipient_id)
@issue = Issue.find(issue_id)
@project = @issue.project
@target_url = namespace_project_issue_url(@project.namespace, @project, @issue)
- yield
-
- SentNotification.record(@issue, recipient_id, reply_key)
+ @sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 7923fb770d0..325996e2e16 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -1,77 +1,64 @@
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
mail_new_thread(@merge_request,
from: sender(@merge_request.author_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@updated_by = User.find updated_by_user_id
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
- @project = @merge_request.project
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
-
- SentNotification.record(@merge_request, recipient_id, reply_key)
end
def merge_request_status_email(recipient_id, merge_request_id, status, updated_by_user_id)
- @merge_request = MergeRequest.find(merge_request_id)
+ setup_merge_request_mail(merge_request_id, recipient_id)
+
@mr_status = status
- @project = @merge_request.project
@updated_by = User.find updated_by_user_id
- @target_url = namespace_project_merge_request_url(@project.namespace,
- @project,
- @merge_request)
mail_answer_thread(@merge_request,
from: sender(updated_by_user_id),
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
+ end
+
+ private
+
+ def setup_merge_request_mail(merge_request_id, recipient_id)
+ @merge_request = MergeRequest.find(merge_request_id)
+ @project = @merge_request.project
+ @target_url = namespace_project_merge_request_url(@project.namespace,
+ @project,
+ @merge_request)
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index 65f37e92677..f9650df9a74 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,31 +1,31 @@
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @commit = @note.noteable
- @target_url = namespace_project_commit_url(*note_target_url_options)
-
- mail_answer_thread(@commit,
- from: sender(@note.author_id),
- to: recipient(recipient_id),
- subject: subject("#{@commit.title} (#{@commit.short_id})"))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @commit = @note.noteable
+ @target_url = namespace_project_commit_url(*note_target_url_options)
+
+ mail_answer_thread(@commit,
+ from: sender(@note.author_id),
+ to: recipient(recipient_id),
+ subject: subject("#{@commit.title} (#{@commit.short_id})"))
end
def note_issue_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @issue = @note.noteable
- @target_url = namespace_project_issue_url(*note_target_url_options)
- mail_answer_thread(@issue, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @issue = @note.noteable
+ @target_url = namespace_project_issue_url(*note_target_url_options)
+ mail_answer_thread(@issue, note_thread_options(recipient_id))
end
def note_merge_request_email(recipient_id, note_id)
- note_mail_with_notification(note_id, recipient_id) do
- @merge_request = @note.noteable
- @target_url = namespace_project_merge_request_url(*note_target_url_options)
- mail_answer_thread(@merge_request, note_thread_options(recipient_id))
- end
+ setup_note_mail(note_id, recipient_id)
+
+ @merge_request = @note.noteable
+ @target_url = namespace_project_merge_request_url(*note_target_url_options)
+ mail_answer_thread(@merge_request, note_thread_options(recipient_id))
end
private
@@ -42,13 +42,11 @@ module Emails
}
end
- def note_mail_with_notification(note_id, recipient_id)
+ def setup_note_mail(note_id, recipient_id)
@note = Note.find(note_id)
@project = @note.project
- yield
-
- SentNotification.record(@note, recipient_id, reply_key)
+ @sent_notification = SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 3bbdd9cee76..e1cd075a978 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -107,10 +107,9 @@ class Notify < BaseMailer
end
headers["X-GitLab-#{model.class.name}-ID"] = model.id
+ headers['X-GitLab-Reply-Key'] = reply_key
- if reply_key
- headers['X-GitLab-Reply-Key'] = reply_key
-
+ if Gitlab::IncomingEmail.enabled?
address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 1b3ee757040..5375148a654 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -69,7 +69,7 @@ class Ability
subject.group
end
- if group && group.public_profile?
+ if group && group.projects.public_only.any?
[:read_group]
else
[]
@@ -175,7 +175,7 @@ class Ability
:create_merge_request,
:create_wiki,
:manage_builds,
- :download_build_artifacts,
+ :read_build_artifacts,
:push_code
]
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 89b3116b9f2..2bc15c60d57 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -18,4 +18,15 @@ class AbuseReport < ActiveRecord::Base
validates :user, presence: true
validates :message, presence: true
validates :user_id, uniqueness: true
+
+ def remove_user
+ user.block
+ user.destroy
+ end
+
+ 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 7c107da116c..6c6c2468374 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -27,9 +27,20 @@
# admin_notification_email :string(255)
# shared_runners_enabled :boolean default(TRUE), not null
# max_artifacts_size :integer default(100), not null
-# runners_registration_token :string(255)
-# require_two_factor_authentication :boolean default(TRUE)
+# runners_registration_token :string
+# require_two_factor_authentication :boolean default(FALSE)
# two_factor_grace_period :integer default(48)
+# metrics_enabled :boolean default(FALSE)
+# metrics_host :string default("localhost")
+# metrics_username :string
+# metrics_password :string
+# metrics_pool_size :integer default(16)
+# metrics_timeout :integer default(10)
+# metrics_method_call_threshold :integer default(10)
+# recaptcha_enabled :boolean default(FALSE)
+# recaptcha_site_key :string
+# recaptcha_private_key :string
+# metrics_port :integer default(8089)
#
class ApplicationSetting < ActiveRecord::Base
@@ -44,24 +55,32 @@ class ApplicationSetting < ActiveRecord::Base
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,
- url: true,
- 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,
- url: true
+ 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 }
+ 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?
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index ad514706160..61119633717 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -6,7 +6,6 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
@@ -23,7 +22,22 @@ class BroadcastMessage < ActiveRecord::Base
validates :color, allow_blank: true, color: true
validates :font, allow_blank: true, color: true
+ default_value_for :color, '#E75E40'
+ default_value_for :font, '#FFFFFF'
+
def self.current
- where("ends_at > :now AND starts_at < :now", now: Time.zone.now).last
+ where("ends_at > :now AND starts_at <= :now", now: Time.zone.now).last
+ end
+
+ def active?
+ started? && !ended?
+ end
+
+ def started?
+ Time.zone.now >= starts_at
+ end
+
+ def ended?
+ ends_at < Time.zone.now
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 7b89fe069ea..6cc26abce66 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -29,10 +29,13 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
+# artifacts_metadata :text
#
module Ci
class Build < CommitStatus
+ include Gitlab::Application.routes.url_helpers
LAZY_ATTRIBUTES = ['trace']
belongs_to :runner, class_name: 'Ci::Runner'
@@ -48,12 +51,15 @@ module Ci
scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
mount_uploader :artifacts_file, ArtifactUploader
+ mount_uploader :artifacts_metadata, ArtifactUploader
acts_as_taggable
# To prevent db load megabytes of data from trace
default_scope -> { select(Ci::Build.columns_without_lazy) }
+ before_destroy { project }
+
class << self
def columns_without_lazy
(column_names - LAZY_ATTRIBUTES).map do |column_name|
@@ -145,10 +151,6 @@ module Ci
end
end
- def project
- commit.project
- end
-
def project_id
commit.project.id
end
@@ -194,8 +196,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
@@ -204,7 +209,7 @@ module Ci
def trace
trace = raw_trace
- if project && trace.present?
+ if project && trace.present? && project.runners_token.present?
trace.gsub(project.runners_token, 'xxxxxx')
else
trace
@@ -212,8 +217,8 @@ module Ci
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)
@@ -231,6 +236,55 @@ 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
@@ -240,21 +294,18 @@ module Ci
end
def target_url
- Gitlab::Application.routes.url_helpers.
- namespace_project_build_url(project.namespace, 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(project.namespace, project, self)
+ cancel_namespace_project_build_path(project.namespace, project, self)
end
end
def retry_url
if retryable?
- Gitlab::Application.routes.url_helpers.
- retry_namespace_project_build_path(project.namespace, project, self)
+ retry_namespace_project_build_path(project.namespace, project, self)
end
end
@@ -270,20 +321,35 @@ 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
+ def artifacts?
+ artifacts_file.exists?
+ end
+
+ def artifacts_download_url
+ if artifacts?
+ download_namespace_project_build_artifacts_path(project.namespace, project, self)
+ end
+ end
+ def artifacts_browse_url
+ if artifacts_browser_supported?
+ browse_namespace_project_build_artifacts_path(project.namespace, project, self)
+ end
+ end
+
+ def artifacts_browser_supported?
+ artifacts? && artifacts_metadata.exists?
+ end
+
+ def artifacts_metadata_entry(path)
+ Gitlab::Ci::Build::Artifacts::Metadata.new(artifacts_metadata.path, path).to_entry
+ end
private
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 93d9be144e8..7b16f207a26 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -2,11 +2,12 @@
#
# Table name: ci_runner_projects
#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
module Ci
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 23516709a41..2b9a457c8ab 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -2,12 +2,13 @@
#
# Table name: ci_triggers
#
-# id :integer not null, primary key
-# token :string(255)
-# project_id :integer not null
-# deleted_at :datetime
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
module Ci
@@ -32,6 +33,10 @@ module Ci
trigger_requests.last
end
+ def last_used
+ last_trigger_request.try(:created_at)
+ end
+
def short_token
token[0...10]
end
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 56759d3e50f..e786bd7dd93 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -3,12 +3,13 @@
# Table name: ci_variables
#
# id :integer not null, primary key
-# project_id :integer not null
+# project_id :integer
# key :string(255)
# value :text
# encrypted_value :text
# encrypted_value_salt :string(255)
# encrypted_value_iv :string(255)
+# gl_project_id :integer
#
module Ci
@@ -17,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id
+ validates :key,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: /\A[a-zA-Z0-9_]+\z/,
+ message: "can contain only letters, digits and '_'." }
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 21c5c87bc3d..66e0502fc0c 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,30 +1,35 @@
# == Schema Information
#
-# project_id integer
-# status string
-# finished_at datetime
-# trace text
-# created_at datetime
-# updated_at datetime
-# started_at datetime
-# runner_id integer
-# coverage float
-# commit_id integer
-# commands text
-# job_id integer
-# name string
-# deploy boolean default: false
-# options text
-# allow_failure boolean default: false, null: false
-# stage string
-# trigger_request_id integer
-# stage_idx integer
-# tag boolean
-# ref string
-# user_id integer
-# type string
-# target_url string
-# description string
+# Table name: ci_builds
+#
+# id :integer not null, primary key
+# project_id :integer
+# status :string(255)
+# finished_at :datetime
+# trace :text
+# created_at :datetime
+# updated_at :datetime
+# started_at :datetime
+# runner_id :integer
+# coverage :float
+# commit_id :integer
+# commands :text
+# job_id :integer
+# name :string(255)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+# stage_idx :integer
+# tag :boolean
+# ref :string(255)
+# user_id :integer
+# type :string(255)
+# target_url :string(255)
+# description :string(255)
+# artifacts_file :text
+# gl_project_id :integer
#
class CommitStatus < ActiveRecord::Base
@@ -51,6 +56,8 @@ class CommitStatus < ActiveRecord::Base
scope :ordered, -> { order(:ref, :stage_idx, :name) }
scope :for_ref, ->(ref) { where(ref: ref) }
+ AVAILABLE_STATUSES = ['pending', 'running', 'success', 'failed', 'canceled']
+
state_machine :status, initial: :pending do
event :run do
transition pending: :running
@@ -126,7 +133,11 @@ class CommitStatus < ActiveRecord::Base
false
end
- def download_url
+ def artifacts_download_url
+ nil
+ end
+
+ def artifacts_browse_url
nil
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 919833f6df5..04650a9e67a 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -95,14 +95,12 @@ module Issuable
opened? || reopened?
end
- # Deprecated. Still exists to preserve API compatibility.
def downvotes
- 0
+ notes.awards.where(note: "thumbsdown").count
end
- # Deprecated. Still exists to preserve API compatibility.
def upvotes
- 0
+ notes.awards.where(note: "thumbsup").count
end
def subscribed?(user)
@@ -121,6 +119,12 @@ module Issuable
update(subscribed: !subscribed?(user))
end
+ def unsubscribe(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: false)
+ end
+
def to_hook_data(user)
{
object_kind: self.class.name.underscore,
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 6316ee208b5..98f71ae8cb0 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -51,8 +51,11 @@ module Mentionable
else
self.class.mentionable_attrs.each do |attr, options|
text = send(attr)
- options[:cache_key] = [self, attr] if options.delete(:cache) && self.persisted?
- ext.analyze(text, options)
+
+ context = options.dup
+ context[:cache_key] = [self, attr] if context.delete(:cache) && self.persisted?
+
+ ext.analyze(text, context)
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 7391a77383c..8b47b9e0abd 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -11,6 +11,7 @@ module Sortable
default_scope { order_id_desc }
scope :order_id_desc, -> { reorder(id: :desc) }
+ scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
@@ -28,6 +29,8 @@ module Sortable
when 'updated_desc' then order_updated_desc
when 'created_asc' then order_created_asc
when 'created_desc' then order_created_desc
+ when 'id_desc' then order_id_desc
+ when 'id_asc' then order_id_asc
else
all
end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 12c934e2494..97f4f03a9a5 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
class GenericCommitStatus < CommitStatus
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index af1d7562ebe..7ee276255a0 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -121,9 +121,9 @@ class GlobalMilestone
def expires_at
if due_date
if due_date.past?
- "expired at #{due_date.stamp("Aug 21, 2011")}"
+ "expired on #{due_date.to_s(:medium)}"
else
- "expires at #{due_date.stamp("Aug 21, 2011")}"
+ "expires on #{due_date.to_s(:medium)}"
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 1b5b875a19e..5a31b46920c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'carrierwave/orm/activerecord'
@@ -50,10 +49,6 @@ class Group < Namespace
User.reference_pattern
end
- def public_and_given_groups(ids)
- where('public IS TRUE OR namespaces.id IN (?)', ids)
- end
-
def visible_to_user(user)
where(id: user.authorized_groups.select(:id).reorder(nil))
end
@@ -125,10 +120,6 @@ class Group < Namespace
end
end
- def public_profile?
- self.public || projects.public_only.any?
- end
-
def post_create_hook
Gitlab::AppLogger.info("Group \"#{name}\" was created")
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 22638057773..fa18ba5dbbe 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class ProjectHook < WebHook
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 09bb3ee52a2..b333a337347 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class ServiceHook < WebHook
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 2f63c59b07e..d81512fae5d 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class SystemHook < WebHook
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 40eb0e20b4b..3bb50c63cac 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -15,6 +15,7 @@
# tag_push_events :boolean default(FALSE)
# note_events :boolean default(FALSE), not null
# enable_ssl_verification :boolean default(TRUE)
+# build_events :boolean default(FALSE), not null
#
class WebHook < ActiveRecord::Base
@@ -47,8 +48,8 @@ class WebHook < ActiveRecord::Base
else
post_url = url.gsub("#{parsed_url.userinfo}@", "")
auth = {
- username: URI.decode(parsed_url.user),
- password: URI.decode(parsed_url.password),
+ username: CGI.unescape(parsed_url.user),
+ password: CGI.unescape(parsed_url.password),
}
response = WebHook.post(post_url,
body: data.to_json,
@@ -60,7 +61,7 @@ class WebHook < ActiveRecord::Base
basic_auth: auth)
end
- [response.code == 200, ActionView::Base.full_sanitizer.sanitize(response.to_s)]
+ [(response.code >= 200 && response.code < 300), ActionView::Base.full_sanitizer.sanitize(response.to_s)]
rescue SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Net::OpenTimeout => e
logger.error("WebHook Error => #{e}")
[false, e.to_s]
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 8bcdc194953..e1915b079d4 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -18,4 +18,8 @@ class Identity < ActiveRecord::Base
validates :provider, presence: true
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
validates :user_id, uniqueness: { scope: :provider }
+
+ def ldap?
+ provider.starts_with?('ldap')
+ end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 80ecd15077f..7beba984608 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -33,7 +33,9 @@ class Issue < ActiveRecord::Base
belongs_to :project
validates :project, presence: true
- scope :of_group, ->(group) { where(project_id: group.project_ids) }
+ scope :of_group,
+ ->(group) { where(project_id: group.projects.select(:id).reorder(nil)) }
+
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
@@ -83,10 +85,10 @@ class Issue < ActiveRecord::Base
reference
end
- def referenced_merge_requests
+ def referenced_merge_requests(current_user = nil)
Gitlab::ReferenceExtractor.lazily do
[self, *notes].flat_map do |note|
- note.all_references.merge_requests
+ note.all_references(current_user).merge_requests
end
end.sort_by(&:iid)
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index fe87b820e98..a9fc6bc167a 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -2,28 +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
-# merge_error :string(255)
-# merge_params :text (serialized to hash)
-# merge_when_build_succeeds :boolean default(false), not null
-# merge_user_id :integer
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
#
require Rails.root.join("app/models/commit")
@@ -131,7 +131,7 @@ class MergeRequest < ActiveRecord::Base
validate :validate_branches
validate :validate_fork
- scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) }
+ scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.projects.select(:id).reorder(nil)) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
scope :by_milestone, ->(milestone) { where(milestone_id: milestone) }
@@ -229,6 +229,8 @@ class MergeRequest < ActiveRecord::Base
end
def check_if_can_be_merged
+ return unless unchecked?
+
can_be_merged =
project.repository.can_be_merged?(source_sha, target_branch)
@@ -252,7 +254,11 @@ class MergeRequest < ActiveRecord::Base
end
def mergeable?
- open? && !work_in_progress? && can_be_merged?
+ return false unless open? && !work_in_progress?
+
+ check_if_can_be_merged
+
+ can_be_merged?
end
def gitlab_merge_status
@@ -452,6 +458,10 @@ class MergeRequest < ActiveRecord::Base
!source_branch_exists? || !target_branch_exists?
end
+ def broken?
+ self.commits.blank? || branch_missing? || cannot_be_merged?
+ end
+
def can_be_merged_by?(user)
::Gitlab::GitAccess.new(user, project).can_push_to_branch?(target_branch)
end
@@ -508,10 +518,6 @@ class MergeRequest < ActiveRecord::Base
@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
-
def diff_range
[last_commit.parent, first_commit]
end
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index d8c7536cd31..c9a0ad8b9b6 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -22,6 +22,7 @@ class Milestone < ActiveRecord::Base
include InternalId
include Sortable
+ include Referable
include StripAttribute
belongs_to :project
@@ -61,6 +62,27 @@ class Milestone < ActiveRecord::Base
end
end
+ def self.reference_pattern
+ nil
+ end
+
+ def self.link_reference_pattern
+ super("milestones", /(?<milestone>\d+)/)
+ end
+
+ def to_reference(from_project = nil)
+ escaped_title = self.title.gsub("]", "\\]")
+
+ h = Gitlab::Application.routes.url_helpers
+ url = h.namespace_project_milestone_url(self.project.namespace, self.project, self)
+
+ "[#{escaped_title}](#{url})"
+ end
+
+ def reference_link_text(from_project = nil)
+ self.title
+ end
+
def expired?
if due_date
due_date.past?
@@ -90,9 +112,9 @@ class Milestone < ActiveRecord::Base
def expires_at
if due_date
if due_date.past?
- "expired at #{due_date.stamp("Aug 21, 2011")}"
+ "expired on #{due_date.to_s(:medium)}"
else
- "expires at #{due_date.stamp("Aug 21, 2011")}"
+ "expires on #{due_date.to_s(:medium)}"
end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index adafabbec07..bdb33f37495 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
class Namespace < ActiveRecord::Base
diff --git a/app/models/note.rb b/app/models/note.rb
index 1222d99cf1f..3e1375e5ad6 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -346,20 +346,22 @@ class Note < ActiveRecord::Base
read_attribute(:system)
end
- # Deprecated. Still exists to preserve API compatibility.
def downvote?
- false
+ is_award && note == "thumbsdown"
end
- # Deprecated. Still exists to preserve API compatibility.
def upvote?
- false
+ is_award && note == "thumbsup"
end
def editable?
!system? && !is_award
end
+ def cross_reference_not_visible_for?(user)
+ cross_reference? && referenced_mentionables(user).empty?
+ end
+
# Checks if note is an award added as a comment
#
# If note is an award, this method sets is_award to true
diff --git a/app/models/project.rb b/app/models/project.rb
index 75f85310d5f..7e131151513 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -29,6 +29,13 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
#
require 'carrierwave/orm/activerecord'
@@ -43,6 +50,7 @@ class Project < ActiveRecord::Base
include Sortable
include AfterCommitQueue
include CaseSensitivity
+ include TokenAuthenticatable
extend Gitlab::ConfigHelper
@@ -81,6 +89,7 @@ class Project < ActiveRecord::Base
acts_as_taggable_on :tags
attr_accessor :new_default_branch
+ attr_accessor :old_path_with_namespace
# Relations
belongs_to :creator, foreign_key: 'creator_id', class_name: 'User'
@@ -185,10 +194,8 @@ class Project < ActiveRecord::Base
if: ->(project) { project.avatar.present? && project.avatar_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
- before_validation :set_runners_token_token
- def set_runners_token_token
- self.runners_token = SecureRandom.hex(15) if self.runners_token.blank?
- end
+ add_authentication_token_field :runners_token
+ before_save :ensure_runners_token
mount_uploader :avatar, AvatarUploader
@@ -390,7 +397,7 @@ class Project < ActiveRecord::Base
result.password = '*****' unless result.password.nil?
result.to_s
rescue
- original_url
+ self.import_url
end
def check_limit
@@ -555,7 +562,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
@@ -699,6 +708,11 @@ class Project < ActiveRecord::Base
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
send_move_instructions(old_path_with_namespace)
reset_events_cache
+
+ @old_path_with_namespace = old_path_with_namespace
+
+ SystemHooksService.new.execute_hooks_for(self, :rename)
+
@repository = nil
rescue
# Returning false does not rollback after_* transaction but gives
@@ -767,6 +781,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
@@ -883,4 +899,8 @@ class Project < ActiveRecord::Base
return true unless forked?
Gitlab::VisibilityLevel.allowed_fork_levels(forked_from_project.visibility_level).include?(level.to_i)
end
+
+ def runners_token
+ ensure_runners_token!
+ end
end
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index e6e16058d41..792ad804575 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -16,7 +16,9 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
+
require 'asana'
class AsanaService < Service
@@ -40,8 +42,8 @@ get the commit comment added to it.
You can also close a task with a message containing: `fix #123456`.
-You can find your Api Keys here:
-http://developer.asana.com/documentation/#api_keys'
+You can create a Personal Access Token here:
+http://app.asana.com/-/account_api'
end
def to_param
@@ -53,14 +55,12 @@ http://developer.asana.com/documentation/#api_keys'
{
type: 'text',
name: 'api_key',
- placeholder: 'User API token. User must have access to task,
-all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
},
{
type: 'text',
name: 'restrict_to_branch',
- placeholder: 'Comma-separated list of branches which will be
-automatically inspected. Leave blank to include all branches.'
+ placeholder: 'Comma-separated list of branches which will be automatically inspected. Leave blank to include all branches.'
}
]
end
@@ -69,58 +69,58 @@ automatically inspected. Leave blank to include all branches.'
%w(push)
end
+ def client
+ @_client ||= begin
+ Asana::Client.new do |c|
+ c.authentication :access_token, api_key
+ end
+ end
+ end
+
def execute(data)
return unless supported_events.include?(data[:object_kind])
- Asana.configure do |client|
- client.api_key = api_key
- end
-
- user = data[:user_name]
+ # check the branch restriction is poplulated and branch is not included
branch = Gitlab::Git.ref_name(data[:ref])
-
branch_restriction = restrict_to_branch.to_s
-
- # check the branch restriction is poplulated and branch is not included
if branch_restriction.length > 0 && branch_restriction.index(branch).nil?
return
end
+ user = data[:user_name]
project_name = project.name_with_namespace
- push_msg = user + ' pushed to branch ' + branch + ' of ' + project_name
data[:commits].each do |commit|
- check_commit(' ( ' + commit[:url] + ' ): ' + commit[:message], push_msg)
+ push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
+ check_commit(commit[:message], push_msg)
end
end
def check_commit(message, push_msg)
- task_list = []
- close_list = []
-
- message.split("\n").each do |line|
- # look for a task ID or a full Asana url
- task_list.concat(line.scan(/#(\d+)/))
- task_list.concat(line.scan(/https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)/))
- # look for a word starting with 'fix' followed by a task ID
- close_list.concat(line.scan(/(fix\w*)\W*#(\d+)/i))
- end
-
- # post commit to every taskid found
- task_list.each do |taskid|
- task = Asana::Task.find(taskid[0])
-
- if task
- task.create_story(text: push_msg + ' ' + message)
- end
- end
-
- # close all tasks that had 'fix(ed/es/ing) #:id' in them
- close_list.each do |taskid|
- task = Asana::Task.find(taskid.last)
-
- if task
- task.modify(completed: true)
+ # matches either:
+ # - #1234
+ # - https://app.asana.com/0/0/1234
+ # optionally preceded with:
+ # - fix/ed/es/ing
+ # - close/s/d
+ # - closing
+ issue_finder = /(fix\w*|clos[ei]\w*+)?\W*(?:https:\/\/app\.asana\.com\/\d+\/\d+\/(\d+)|#(\d+))/i
+
+ message.scan(issue_finder).each do |tuple|
+ # tuple will be
+ # [ 'fix', 'id_from_url', 'id_from_pound' ]
+ taskid = tuple[2] || tuple[1]
+
+ begin
+ task = Asana::Task.find_by_id(client, taskid)
+ task.add_comment(text: "#{push_msg} #{message}")
+
+ if tuple[0]
+ task.update(completed: true)
+ end
+ rescue => e
+ Rails.logger.error(e.message)
+ next
end
end
end
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index fb7e0c0fb0d..29d841faed8 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class AssemblaService < Service
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index aa8746beb80..9e7f642180e 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class BambooService < CiService
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 199ee3a9d0d..3efbfd2eec3 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "addressable/uri"
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index 8247c79fc33..f6313255cbb 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class BuildsEmailService < Service
@@ -72,12 +73,16 @@ class BuildsEmailService < Service
when 'success'
!notify_only_broken_builds?
when 'failed'
- true
+ !allow_failure?(data)
else
false
end
end
+ def allow_failure?(data)
+ data[:build_allow_failure] == true
+ end
+
def all_recipients(data)
all_recipients = recipients.split(',')
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index e591afdda64..6e8f0842524 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class CampfireService < Service
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index 88186113c68..c3f70d1f972 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# Base class for CI services
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 7c2027c18e6..88a3e9218cb 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class CustomIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 08e5ccb3855..b4724bb647e 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class DroneCiService < CiService
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index 8f5d8b086eb..b831577cd97 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class EmailsOnPushService < Service
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index 74c57949b4d..b402b68665a 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class ExternalWikiService < Service
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 15c7c907f7e..8605ce66e48 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "flowdock-git-hook"
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 202fee042e3..61babe9cfe5 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require "gemnasium/gitlab_service"
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index b64d97ce75d..33f0d7ea01a 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 9558292fea3..7aa04309f54 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class GitlabIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 1e1686a11c6..0e3fa4a40fe 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class HipchatService < Service
@@ -119,13 +120,13 @@ class HipchatService < Service
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
message << "pushed new #{ref_type} <a href=\""\
- "#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\
+ "#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"\
" to #{project_link}\n"
elsif Gitlab::Git.blank_ref?(after)
message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n"
else
message << "pushed to #{ref_type} <a href=\""\
- "#{project.web_url}/commits/#{URI.escape(ref)}\">#{ref}</a> "
+ "#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/,'')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
@@ -254,8 +255,8 @@ class HipchatService < Service
status = data[:commit][:status]
duration = data[:commit][:duration]
- branch_link = "<a href=\"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"
- commit_link = "<a href=\"#{project_url}/commit/#{URI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+ branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
+ commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index d24aa317cf3..04c714bfaad 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'uri'
@@ -72,9 +73,10 @@ class IrkerService < Service
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
'the channel name; if the channel is protected by a secret password, ' \
- ' append "?key=secretpassword" to the URI. Note that if you specify a ' \
- ' default IRC URI to prepend before each recipient, you can just give ' \
- ' a channel name.' },
+ ' append "?key=secretpassword" to the URI (Note that due to a bug, if you ' \
+ ' want to use a password, you have to omit the "#" on the channel). If you ' \
+ ' specify a default IRC URI to prepend before each recipient, you can just ' \
+ ' give a channel name.' },
{ type: 'checkbox', name: 'colorize_messages' },
]
end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 936e574cccd..ed201979d39 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class IssueTrackerService < Service
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index e216f406e1c..f6571fc063e 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class JiraService < IssueTrackerService
@@ -39,15 +40,10 @@ class JiraService < IssueTrackerService
end
def help
- line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
+ 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
'allow a user to easily navigate to the Jira issue tracker. See the '\
'[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
'for details.'
-
- line2 = 'Support for referencing commits and automatic closing of Jira issues directly '\
- 'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)'
-
- [line1, line2].join("\n\n")
end
def title
@@ -120,6 +116,7 @@ class JiraService < IssueTrackerService
end
def test_settings
+ return unless api_url.present?
result = JiraService.get(
jira_api_test_url,
headers: {
@@ -217,6 +214,7 @@ class JiraService < IssueTrackerService
end
def send_message(url, message)
+ return unless api_url.present?
result = JiraService.post(
url,
body: message,
@@ -242,6 +240,7 @@ class JiraService < IssueTrackerService
end
def existing_comment?(issue_name, new_comment)
+ return unless api_url.present?
result = JiraService.get(
comment_url(issue_name),
headers: {
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index ade9ee97873..c9a890c7e3f 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class PivotaltrackerService < Service
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 53edf522e9a..3d7e8bbee61 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class PushoverService < Service
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index dd9ba97ee1f..de974354c77 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class RedmineService < IssueTrackerService
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 375b4534d07..d89cf6d17b2 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class SlackService < Service
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index a63700693d7..b8e9416131a 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
class TeamcityService < CiService
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index b5fec38378b..8ce47495971 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -38,6 +38,10 @@ class ProjectWiki
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
end
+ def wiki_base_path
+ ["/", @project.path_with_namespace, "/wikis"].join('')
+ end
+
# Returns the Gollum::Wiki object.
def wiki
@wiki ||= begin
diff --git a/app/models/repository.rb b/app/models/repository.rb
index a9bf4eb4033..d9ff71c01ed 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -92,9 +92,12 @@ class Repository
commits
end
- def find_commits_by_message(query)
+ def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
+ ref ||= root_ref
+
# Limited to 1000 commits for now, could be parameterized?
- args = %W(#{Gitlab.config.git.bin_path} log --pretty=%H --max-count 1000 --grep=#{query})
+ args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
+ args = args.concat(%W(-- #{path})) if path.present?
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
commits = git_log_results.map { |c| commit(c) }
@@ -176,17 +179,41 @@ class Repository
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|
unless cache.exist?(key)
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
@@ -203,6 +230,14 @@ class Repository
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
@@ -210,6 +245,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
@@ -644,6 +684,11 @@ class Repository
end
end
+ def ls_files(ref)
+ actual_ref = ref || root_ref
+ raw_repository.ls_files(actual_ref)
+ end
+
private
def cache
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index f36eda1531b..77115597d71 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -25,8 +25,6 @@ class SentNotification < ActiveRecord::Base
class << self
def reply_key
- return nil unless Gitlab::IncomingEmail.enabled?
-
SecureRandom.hex(16)
end
@@ -59,11 +57,15 @@ class SentNotification < ActiveRecord::Base
def record_note(note, recipient_id, reply_key, params = {})
params[:line_code] = note.line_code
-
+
record(note.noteable, recipient_id, reply_key, params)
end
end
+ def unsubscribable?
+ !for_commit?
+ end
+
def for_commit?
noteable_type == "Commit"
end
@@ -75,4 +77,8 @@ class SentNotification < ActiveRecord::Base
super
end
end
+
+ def to_param
+ self.reply_key
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index d3bf7f0ebd1..24f4bf7646e 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
# To add new service you should build a class inherited from Service
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 df87f3b79bd..592468933ed 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,62 +2,63 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# unlock_token :string(255)
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
-# consumed_timestep :integer
-# layout :integer default(0)
-# hide_project_limit :boolean default(FALSE)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
+# unlock_token :string
+# otp_grace_period_started_at :datetime
#
require 'carrierwave/orm/activerecord'
@@ -195,10 +196,22 @@ class User < ActiveRecord::Base
state_machine :state, initial: :active do
event :block do
transition active: :blocked
+ transition ldap_blocked: :blocked
+ end
+
+ event :ldap_block do
+ transition active: :ldap_blocked
end
event :activate do
transition blocked: :active
+ transition ldap_blocked: :active
+ end
+
+ state :blocked, :ldap_blocked do
+ def blocked?
+ true
+ end
end
end
@@ -206,7 +219,7 @@ class User < ActiveRecord::Base
# Scopes
scope :admins, -> { where(admin: true) }
- scope :blocked, -> { with_state(:blocked) }
+ scope :blocked, -> { with_states(:blocked, :ldap_blocked) }
scope :active, -> { with_state(:active) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
@@ -352,10 +365,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
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index e9413c34bae..2a65f0431c4 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -169,7 +169,7 @@ class WikiPage
private
def set_attributes
- attributes[:slug] = @page.escaped_url_path
+ attributes[:slug] = @page.url_path
attributes[:title] = @page.title
attributes[:format] = @page.format
end
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index f139872c728..c0e08a151f2 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -31,7 +31,6 @@ class CreateBranchService < BaseService
if new_branch
push_data = build_push_data(project, current_user, new_branch)
- EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :push_hooks)
project.execute_services(push_data.dup, :push_hooks)
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 2452999382a..55985380d31 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -23,6 +23,7 @@ class CreateTagService < BaseService
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
+ CreateCommitBuildsService.new.execute(project, current_user, push_data)
if release_description
CreateReleaseService.new(@project, @current_user).
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 22bf9dd935e..004b3ce7286 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -27,7 +27,6 @@ class DeleteBranchService < BaseService
if repository.rm_branch(current_user, branch_name)
push_data = build_push_data(branch)
- EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :push_hooks)
project.execute_services(push_data.dup, :push_hooks)
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index cabc3d8fabb..e8bef250d8b 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -44,7 +44,7 @@ module MergeRequests
def after_merge
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
- if params[:should_remove_source_branch]
+ if params[:should_remove_source_branch].present?
DeleteBranchService.new(@merge_request.source_project, current_user).
execute(merge_request.source_branch)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index bdf7b3ad2bb..e4edc55bf69 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -413,6 +413,7 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients.delete(current_user)
+ recipients = recipients.uniq
recipients
end
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
index 99f22293d0d..6386f57fb0d 100644
--- a/app/services/projects/download_service.rb
+++ b/app/services/projects/download_service.rb
@@ -16,13 +16,7 @@ module Projects
uploader.download!(@url)
uploader.store!
- filename = uploader.image? ? uploader.file.basename : uploader.file.filename
-
- {
- 'alt' => filename,
- 'url' => uploader.secure_url,
- 'is_image' => uploader.image?
- }
+ uploader.to_h
end
private
diff --git a/app/services/projects/housekeeping_service.rb b/app/services/projects/housekeeping_service.rb
new file mode 100644
index 00000000000..0db85ac2142
--- /dev/null
+++ b/app/services/projects/housekeeping_service.rb
@@ -0,0 +1,20 @@
+# Projects::HousekeepingService class
+#
+# Used for git housekeeping
+#
+# Ex.
+# Projects::HousekeepingService.new(project).execute
+#
+module Projects
+ class HousekeepingService < BaseService
+ include Gitlab::ShellAdapter
+
+ def initialize(project)
+ @project = project
+ end
+
+ def execute
+ GitlabShellWorker.perform_async(:gc, @project.path_with_namespace)
+ end
+ end
+end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 64ea6dd42eb..2e734654466 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -55,6 +55,9 @@ module Projects
# Move uploads
Gitlab::UploadsTransfer.new.move_project(project.path, old_namespace.path, new_namespace.path)
+ project.old_path_with_namespace = old_path
+
+ SystemHooksService.new.execute_hooks_for(project, :transfer)
true
end
end
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index 279550d6f4a..012e82a7704 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -10,13 +10,7 @@ module Projects
uploader = FileUploader.new(@project)
uploader.store!(@file)
- filename = uploader.image? ? uploader.file.basename : uploader.file.filename
-
- {
- alt: filename,
- url: uploader.secure_url,
- is_image: uploader.image?
- }
+ uploader.to_h
end
private
diff --git a/app/services/repair_ldap_blocked_user_service.rb b/app/services/repair_ldap_blocked_user_service.rb
new file mode 100644
index 00000000000..863cef7ff61
--- /dev/null
+++ b/app/services/repair_ldap_blocked_user_service.rb
@@ -0,0 +1,17 @@
+class RepairLdapBlockedUserService
+ attr_accessor :user
+
+ def initialize(user)
+ @user = user
+ end
+
+ def execute
+ user.block if ldap_hard_blocked?
+ end
+
+ private
+
+ def ldap_hard_blocked?
+ user.ldap_blocked? && !user.ldap_user?
+ end
+end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 8b5143e1eb7..ea2b26ccb52 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -18,7 +18,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema
+ created_at: model.created_at.xmlschema,
+ updated_at: model.updated_at.xmlschema
}
case model
@@ -34,11 +35,20 @@ class SystemHooksService
end
when Project
data.merge!(project_data(model))
+
+ if event == :rename || event == :transfer
+ data.merge!({
+ old_path_with_namespace: model.old_path_with_namespace
+ })
+ end
+
+ data
when User
data.merge!({
name: model.name,
email: model.email,
- user_id: model.id
+ user_id: model.id,
+ username: model.username
})
when ProjectMember
data.merge!(project_member_data(model))
@@ -90,8 +100,10 @@ class SystemHooksService
project_path: model.project.path,
project_path_with_namespace: model.project.path_with_namespace,
project_id: model.project.id,
+ user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
+ user_id: model.user.id,
access_level: model.human_access,
project_visibility: Project.visibility_levels.key(model.project.visibility_level_field).downcase
}
@@ -102,6 +114,7 @@ class SystemHooksService
group_name: model.group.name,
group_path: model.group.path,
group_id: model.group.id,
+ user_username: model.user.username,
user_name: model.user.name,
user_email: model.user.email,
user_id: model.user.id,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 98a71cbf1ad..1083bcec054 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -41,7 +41,7 @@ class SystemNoteService
#
# Returns the created Note object
def self.change_assignee(noteable, project, author, assignee)
- body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
+ body = assignee.nil? ? 'Assignee removed' : "Reassigned to #{assignee.to_reference}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -66,7 +66,7 @@ class SystemNoteService
def self.change_label(noteable, project, author, added_labels, removed_labels)
labels_count = added_labels.count + removed_labels.count
- references = ->(label) { "~#{label.id}" }
+ references = ->(label) { label.to_reference(:id) }
added_labels = added_labels.map(&references).join(' ')
removed_labels = removed_labels.map(&references).join(' ')
@@ -103,7 +103,7 @@ class SystemNoteService
# Returns the created Note object
def self.change_milestone(noteable, project, author, milestone)
body = 'Milestone '
- body += milestone.nil? ? 'removed' : "changed to #{milestone.title}"
+ body += milestone.nil? ? 'removed' : "changed to #{milestone.to_reference(project)}"
create_note(noteable: noteable, project: project, author: author, note: body)
end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 1dccc39e7e2..1b0ae6c0056 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -20,16 +20,12 @@ class ArtifactUploader < CarrierWave::Uploader::Base
@build, @field = build, field
end
- def artifacts_path
- File.join(build.created_at.utc.strftime('%Y_%m'), build.project.id.to_s, build.id.to_s)
- end
-
def store_dir
- File.join(ArtifactUploader.artifacts_path, artifacts_path)
+ File.join(self.class.artifacts_path, @build.artifacts_path)
end
def cache_dir
- File.join(ArtifactUploader.artifacts_cache_path, artifacts_path)
+ File.join(self.class.artifacts_cache_path, @build.artifacts_path)
end
def file_storage?
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index ac920119a85..86d24469e05 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -30,4 +30,19 @@ class FileUploader < CarrierWave::Uploader::Base
def secure_url
File.join("/uploads", @secret, file.filename)
end
+
+ def to_h
+ filename = image? ? self.file.basename : self.file.filename
+ escaped_filename = filename.gsub("]", "\\]")
+
+ markdown = "[#{escaped_filename}](#{self.secure_url})"
+ markdown.prepend("!") if image?
+
+ {
+ alt: filename,
+ url: self.secure_url,
+ is_image: image?,
+ markdown: markdown
+ }
+ end
end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 10e35ce665a..7a35958cc5f 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -17,6 +17,7 @@ class NamespaceValidator < ActiveModel::EachValidator
hooks
issues
merge_requests
+ new
notes
profile
projects
diff --git a/app/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..2ab01704b77 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -2,25 +2,30 @@
- user = abuse_report.user
%tr
%td
- - if reporter
- = link_to reporter.name, reporter
+ - if user
+ = link_to user.name, [:admin, user]
+ .light.small
+ Joined #{time_ago_with_tooltip(user.created_at)}
- else
(removed)
%td
- = abuse_report.created_at.to_s(:short)
- %td
- = abuse_report.message
- %td
- - if user
- = link_to user.name, user
+ - if reporter
+ = link_to reporter.name, [:admin, reporter]
- else
(removed)
+ .light.small
+ = time_ago_with_tooltip(abuse_report.created_at)
+ %td
+ = markdown(abuse_report.message.squish!, pipeline: :single_line)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
data: { confirm: "USER #{user.name} WILL BE REMOVED! Are you sure?" }, remote: true, method: :delete, class: "btn btn-xs btn-remove js-remove-tr"
%td
- - if user
+ - if user && !user.blocked?
= link_to 'Block user', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs"
+ - else
+ .btn.btn-xs.disabled
+ Already Blocked
= link_to 'Remove report', [:admin, abuse_report], remote: true, method: :delete, class: "btn btn-xs btn-close js-remove-tr"
diff --git a/app/views/admin/abuse_reports/index.html.haml b/app/views/admin/abuse_reports/index.html.haml
index 40a5fe4628b..bc4a9cedb2c 100644
--- a/app/views/admin/abuse_reports/index.html.haml
+++ b/app/views/admin/abuse_reports/index.html.haml
@@ -6,10 +6,9 @@
%table.table
%thead
%tr
+ %th User
%th Reported by
- %th Reported at
%th Message
- %th User
%th Primary action
%th
= render @abuse_reports
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 3cada08c2ba..83f6814d822 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -149,7 +149,7 @@
.checkbox
= f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled
- Enable shared runners for a new projects
+ Enable shared runners for new projects
.form-group
= f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
@@ -171,20 +171,14 @@
.col-sm-10
= f.text_field :metrics_host, class: 'form-control', placeholder: 'influxdb.example.com'
.form-group
- = f.label :metrics_database, 'InfluxDB database', class: 'control-label col-sm-2'
+ = f.label :metrics_port, 'InfluxDB port', class: 'control-label col-sm-2'
.col-sm-10
- = f.text_field :metrics_database, class: 'form-control', placeholder: 'gitlab'
+ = f.text_field :metrics_port, class: 'form-control', placeholder: '8089'
.help-block
- The name of the InfluxDB database to store data in. Users will have to
- create this database manually, GitLab does not do so automatically.
- .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'
+ 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_pool_size, 'Connection pool size', class: 'control-label col-sm-2'
.col-sm-10
@@ -208,6 +202,35 @@
.help-block
A method call is only tracked when it takes longer to complete than
the given amount of milliseconds.
+ .form-group
+ = f.label :metrics_sample_interval, 'Sampler Interval (sec)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_sample_interval, class: 'form-control'
+ .help-block
+ The sampling interval in seconds. Sampled data includes memory usage,
+ retained Ruby objects, file descriptors and so on.
+
+ %fieldset
+ %legend Spam and Anti-bot Protection
+ .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/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
new file mode 100644
index 00000000000..953b8b69368
--- /dev/null
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -0,0 +1,37 @@
+.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
+ = icon('bullhorn')
+ %span= @broadcast_message.message || "Your message here"
+
+= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-requires-input'} do |f|
+ -if @broadcast_message.errors.any?
+ .alert.alert-danger
+ - @broadcast_message.errors.full_messages.each do |msg|
+ %p= msg
+ .form-group
+ = f.label :message, class: 'control-label'
+ .col-sm-10
+ = f.text_area :message, class: "form-control js-quick-submit", rows: 2, required: true
+ .form-group.js-toggle-colors-container
+ .col-sm-10.col-sm-offset-2
+ = link_to 'Customize colors', '#', class: 'js-toggle-colors-link'
+ .form-group.js-toggle-colors-container.hide
+ = f.label :color, "Background Color", class: 'control-label'
+ .col-sm-10
+ = f.color_field :color, class: "form-control"
+ .form-group.js-toggle-colors-container.hide
+ = f.label :font, "Font Color", class: 'control-label'
+ .col-sm-10
+ = f.color_field :font, class: "form-control"
+ .form-group
+ = f.label :starts_at, class: 'control-label'
+ .col-sm-10.datetime-controls
+ = f.datetime_select :starts_at, {}, class: 'form-control form-control-inline'
+ .form-group
+ = f.label :ends_at, class: 'control-label'
+ .col-sm-10.datetime-controls
+ = f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
+ .form-actions
+ - if @broadcast_message.persisted?
+ = f.submit "Update broadcast message", class: "btn btn-create"
+ - else
+ = f.submit "Add broadcast message", class: "btn btn-create"
diff --git a/app/views/admin/broadcast_messages/edit.html.haml b/app/views/admin/broadcast_messages/edit.html.haml
new file mode 100644
index 00000000000..45e053eb31d
--- /dev/null
+++ b/app/views/admin/broadcast_messages/edit.html.haml
@@ -0,0 +1,3 @@
+- page_title "Broadcast Messages"
+
+= render 'form'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 17dffebd360..49e33698b63 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -1,60 +1,37 @@
- page_title "Broadcast Messages"
+
%h3.page-title
Broadcast Messages
%p.light
- Broadcast messages are displayed for every user and can be used to notify users about scheduled maintenance, recent upgrades and more.
-.broadcast-message-preview
- %i.fa.fa-bullhorn
- %span Your message here
-
-= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal'} do |f|
- -if @broadcast_message.errors.any?
- .alert.alert-danger
- - @broadcast_message.errors.full_messages.each do |msg|
- %p= msg
- .form-group
- = f.label :message, class: 'control-label'
- .col-sm-10
- = f.text_area :message, class: "form-control", rows: 2, required: true
- %div
- = link_to '#', class: 'js-toggle-colors-link' do
- Customize colors
- .form-group.js-toggle-colors-container.hide
- = f.label :color, "Background Color", class: 'control-label'
- .col-sm-10
- = f.color_field :color, value: "#eb9532", class: "form-control"
- .form-group.js-toggle-colors-container.hide
- = f.label :font, "Font Color", class: 'control-label'
- .col-sm-10
- = f.color_field :font, value: "#FFFFFF", class: "form-control"
- .form-group
- = f.label :starts_at, class: 'control-label'
- .col-sm-10.datetime-controls
- = f.datetime_select :starts_at
- .form-group
- = f.label :ends_at, class: 'control-label'
- .col-sm-10.datetime-controls
- = f.datetime_select :ends_at
- .form-actions
- = f.submit "Add broadcast message", class: "btn btn-create"
+ Broadcast messages are displayed for every user and can be used to notify
+ users about scheduled maintenance, recent upgrades and more.
--if @broadcast_messages.any?
- %ul.bordered-list.broadcast-messages
- - @broadcast_messages.each do |broadcast_message|
- %li
- .pull-right
- - if broadcast_message.starts_at
- %strong
- #{broadcast_message.starts_at.to_s(:short)}
- \...
- - if broadcast_message.ends_at
- %strong
- #{broadcast_message.ends_at.to_s(:short)}
- &nbsp;
- = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do
- %i.fa.fa-times.cred
+= render 'form'
- .message= broadcast_message.message
+%br.clearfix
+-if @broadcast_messages.any?
+ %table.table
+ %thead
+ %tr
+ %th Status
+ %th Preview
+ %th Starts
+ %th Ends
+ %th &nbsp;
+ %tbody
+ - @broadcast_messages.each do |message|
+ %tr
+ %td
+ = broadcast_message_status(message)
+ %td
+ = broadcast_message(message)
+ %td
+ = message.starts_at
+ %td
+ = message.ends_at
+ %td
+ = link_to icon('pencil-square-o'), edit_admin_broadcast_message_path(message), title: 'Edit', class: 'btn btn-xs'
+ = link_to icon('times'), admin_broadcast_message_path(message), method: :delete, remote: true, title: 'Remove', class: 'js-remove-tr btn btn-xs btn-danger'
= paginate @broadcast_messages
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 6936e614346..c395bd908c3 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -60,8 +60,8 @@
%td
.pull-right
- - if current_user && can?(current_user, :download_build_artifacts, project) && build.download_url
- = link_to build.download_url, title: 'Download artifacts' do
+ - if current_user && can?(current_user, :read_build_artifacts, project) && build.artifacts?
+ = link_to build.artifacts_download_url, title: 'Download artifacts' do
%i.fa.fa-download
- if current_user && can?(current_user, :manage_builds, build.project)
- if build.active?
diff --git a/app/views/admin/builds/index.html.haml b/app/views/admin/builds/index.html.haml
index 55da06a7fe9..ebf2b7b60e7 100644
--- a/app/views/admin/builds/index.html.haml
+++ b/app/views/admin/builds/index.html.haml
@@ -4,21 +4,21 @@
- 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
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to admin_builds_path do
+ All
+ %span.badge.js-totalbuilds-count= @all_builds.count(:id)
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to admin_builds_path(scope: :running) do
Running
- %span.badge.js-running-count= @all_builds.running_or_pending.count(:id)
+ %span.badge.js-running-count= number_with_delimiter(@all_builds.running_or_pending.count(:id))
%li{class: ('active' if @scope == 'finished')}
= link_to admin_builds_path(scope: :finished) do
Finished
- %span.badge.js-running-count= @all_builds.finished.count(:id)
-
- %li{class: ('active' if @scope == 'all')}
- = link_to admin_builds_path(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
#{(@scope || 'running').capitalize} builds
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 531247e9148..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
@@ -99,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
@@ -107,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
@@ -115,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/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 296497a4cd4..f7fd156b84a 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -30,7 +30,7 @@
%li
%span.light Created on:
%strong
- = @group.created_at.stamp("March 1, 1999")
+ = @group.created_at.to_s(:medium)
.panel.panel-default
.panel-heading
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index d67454c03e7..3c57e3dc174 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -1,5 +1,5 @@
- page_title "Labels"
-= link_to new_admin_label_path, class: "pull-right btn btn-new" do
+= link_to new_admin_label_path, class: "pull-right btn btn-nr btn-new" do
New label
%h3.page-title
Labels
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 1484baa78e0..af9fdeb0734 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,12 +1,13 @@
- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
-%ul.nav.nav-tabs.log-tabs
+%ul.nav-links.log-tabs
- loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
= link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab'
-%p.light To prevent performance issues admin logs output the last 2000 lines
+.gray-content-block
+ To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 5260eadf95b..d734e60682a 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,7 +1,7 @@
- page_title @project.name_with_namespace, "Projects"
%h3.page-title
Project: #{@project.name_with_namespace}
- = link_to edit_project_path(@project), class: "btn pull-right" do
+ = link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
%hr
@@ -38,7 +38,7 @@
%li
%span.light Created on:
%strong
- = @project.created_at.stamp("March 1, 1999")
+ = @project.created_at.to_s(:medium)
%li
%span.light http:
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 5e17b018163..ce5e21e54cc 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -7,12 +7,12 @@
.pull-right
- unless @user == current_user || @user.blocked?
- = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info"
- = link_to edit_admin_user_path(@user), class: "btn btn-grouped" do
+ = link_to 'Impersonate', impersonate_admin_user_path(@user), method: :post, class: "btn btn-nr btn-grouped btn-info"
+ = link_to edit_admin_user_path(@user), class: "btn btn-nr btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
%hr
-%ul.nav.nav-tabs
+%ul.nav-links
= nav_link(path: 'users#show') do
= link_to "Account", admin_user_path(@user)
= nav_link(path: 'users#groups') do
@@ -23,3 +23,4 @@
= link_to "SSH keys", keys_admin_user_path(@user)
= nav_link(controller: :identities) do
= link_to "Identities", admin_user_identities_path(@user)
+.append-bottom-default
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index 7d11edc79e2..6bc217f84cc 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -4,7 +4,7 @@
%ul.well-list
%li
%span.light Member since
- %strong= user.created_at.stamp("Aug 21, 2011")
+ %strong= user.created_at.to_s(:medium)
- unless user.public_email.blank?
%li
%span.light E-mail:
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index bc08458312c..b050a4d01c3 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,101 +1,101 @@
- page_title "Users"
= render 'shared/show_aside'
-.row
- %aside.col-md-3
- .admin-filter
- %ul.nav.nav-pills.nav-stacked
- %li{class: "#{'active' unless params[:filter]}"}
- = link_to admin_users_path do
- Active
- %small.pull-right= User.active.count
- %li{class: "#{'active' if params[:filter] == "admins"}"}
- = link_to admin_users_path(filter: "admins") do
- Admins
- %small.pull-right= User.admins.count
- %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
- = link_to admin_users_path(filter: 'two_factor_enabled') do
- 2FA Enabled
- %small.pull-right= User.with_two_factor.count
- %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
- = link_to admin_users_path(filter: 'two_factor_disabled') do
- 2FA Disabled
- %small.pull-right= User.without_two_factor.count
- %li{class: "#{'active' if params[:filter] == "blocked"}"}
- = link_to admin_users_path(filter: "blocked") do
- Blocked
- %small.pull-right= User.blocked.count
- %li{class: "#{'active' if params[:filter] == "wop"}"}
- = link_to admin_users_path(filter: "wop") do
- Without projects
- %small.pull-right= User.without_projects.count
- %hr
- = form_tag admin_users_path, method: :get, class: 'form-inline' do
- .form-group
- = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
- = hidden_field_tag "filter", params[:filter]
- = button_tag class: 'btn btn-primary' do
- %i.fa.fa-search
- %hr
- = link_to 'Reset', admin_users_path, class: "btn btn-cancel"
+.admin-filter
+ %ul.nav-links
+ %li{class: "#{'active' unless params[:filter]}"}
+ = link_to admin_users_path do
+ Active
+ %small.badge= number_with_delimiter(User.active.count)
+ %li{class: "#{'active' if params[:filter] == "admins"}"}
+ = link_to admin_users_path(filter: "admins") do
+ Admins
+ %small.badge= number_with_delimiter(User.admins.count)
+ %li.filter-two-factor-enabled{class: "#{'active' if params[:filter] == 'two_factor_enabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_enabled') do
+ 2FA Enabled
+ %small.badge= number_with_delimiter(User.with_two_factor.count)
+ %li.filter-two-factor-disabled{class: "#{'active' if params[:filter] == 'two_factor_disabled'}"}
+ = link_to admin_users_path(filter: 'two_factor_disabled') do
+ 2FA Disabled
+ %small.badge= number_with_delimiter(User.without_two_factor.count)
+ %li{class: "#{'active' if params[:filter] == "blocked"}"}
+ = link_to admin_users_path(filter: "blocked") do
+ Blocked
+ %small.badge= number_with_delimiter(User.blocked.count)
+ %li{class: "#{'active' if params[:filter] == "wop"}"}
+ = link_to admin_users_path(filter: "wop") do
+ Without projects
+ %small.badge= number_with_delimiter(User.without_projects.count)
- %section.col-md-9
- .panel.panel-default
- .panel-heading
- Users (#{@users.total_count})
- .panel-head-actions
- .dropdown.inline
- %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_name
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
- = sort_title_name
- = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
- = sort_title_recently_signin
- = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
- = sort_title_oldest_signin
- = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
- = sort_title_recently_created
- = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
- = sort_title_oldest_created
- = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
- = sort_title_recently_updated
- = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
- = sort_title_oldest_updated
-
- = link_to 'New User', new_admin_user_path, class: "btn btn-new btn-sm"
- %ul.well-list
- - @users.each do |user|
+ .gray-content-block.second-block
+ .pull-right
+ .dropdown.inline
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %span.light sort:
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_name
+ %b.caret
+ %ul.dropdown-menu
%li
- .list-item-name
- - if user.blocked?
- %i.fa.fa-lock.cred
+ = link_to admin_users_path(sort: sort_value_name, filter: params[:filter]) do
+ = sort_title_name
+ = link_to admin_users_path(sort: sort_value_recently_signin, filter: params[:filter]) do
+ = sort_title_recently_signin
+ = link_to admin_users_path(sort: sort_value_oldest_signin, filter: params[:filter]) do
+ = sort_title_oldest_signin
+ = link_to admin_users_path(sort: sort_value_recently_created, filter: params[:filter]) do
+ = sort_title_recently_created
+ = link_to admin_users_path(sort: sort_value_oldest_created, filter: params[:filter]) do
+ = sort_title_oldest_created
+ = link_to admin_users_path(sort: sort_value_recently_updated, filter: params[:filter]) do
+ = sort_title_recently_updated
+ = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do
+ = sort_title_oldest_updated
+
+ = link_to 'New User', new_admin_user_path, class: "btn btn-new"
+ = form_tag admin_users_path, method: :get, class: 'form-inline' do
+ .form-group
+ = search_field_tag :name, params[:name], placeholder: 'Name, email or username', class: 'form-control', spellcheck: false
+ = hidden_field_tag "filter", params[:filter]
+ = button_tag class: 'btn btn-primary' do
+ %i.fa.fa-search
+
+
+.panel.panel-default
+ %ul.well-list
+ - @users.each do |user|
+ %li
+ .list-item-name
+ - if user.blocked?
+ %i.fa.fa-lock.cred
+ - else
+ %i.fa.fa-user.cgreen
+ = link_to user.name, [:admin, user]
+ - if user.admin?
+ %strong.cred (Admin)
+ - if user == current_user
+ %span.cred It's you!
+ .pull-right
+ %span.light
+ %i.fa.fa-envelope
+ = mail_to user.email, user.email, class: 'light'
+ &nbsp;
+ .pull-right
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: 'btn-grouped btn btn-xs'
+ - unless user == current_user
+ - if user.ldap_blocked?
+ = link_to '#', title: 'Cannot unblock LDAP blocked users', data: {toggle: 'tooltip'}, class: 'btn-grouped btn btn-xs btn-success disabled' do
+ %i.fa.fa-lock
+ Unblock
+ - elsif user.blocked?
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success'
- else
- %i.fa.fa-user.cgreen
- = link_to user.name, [:admin, user]
- - if user.admin?
- %strong.cred (Admin)
- - if user == current_user
- %span.cred It's you!
- .pull-right
- %span.light
- %i.fa.fa-envelope
- = mail_to user.email, user.email, class: 'light'
- &nbsp;
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-xs"
- - unless user == current_user
- - if user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success"
- - else
- = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-xs btn-warning"
- - if user.access_locked?
- = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' }
- - if user.can_be_removed?
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
- = paginate @users, theme: "gitlab"
+ = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: 'btn-grouped btn btn-xs btn-warning'
+ - if user.access_locked?
+ = link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
+ - if user.can_be_removed?
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: 'btn-grouped btn btn-xs btn-remove'
+= paginate @users, theme: "gitlab"
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 0848504b7a6..2bdbae19588 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -58,12 +58,12 @@
%li
%span.light Member since:
%strong
- = @user.created_at.stamp("Nov 12, 2031")
+ = @user.created_at.to_s(:medium)
- if @user.confirmed_at
%li
%span.light Confirmed at:
%strong
- = @user.confirmed_at.stamp("Nov 12, 2031")
+ = @user.confirmed_at.to_s(:medium)
- else
%li
%span.light Confirmed:
@@ -71,10 +71,26 @@
No
%li
+ %span.light Current sign-in IP:
+ %strong
+ - if @user.current_sign_in_ip
+ = @user.current_sign_in_ip
+ - else
+ never
+
+ %li
%span.light Current sign-in at:
%strong
- if @user.current_sign_in_at
- = @user.current_sign_in_at.stamp("Nov 12, 2031")
+ = @user.current_sign_in_at.to_s(:medium)
+ - else
+ never
+
+ %li
+ %span.light Last sign-in IP:
+ %strong
+ - if @user.last_sign_in_ip
+ = @user.last_sign_in_ip
- else
never
@@ -82,7 +98,7 @@
%span.light Last sign-in at:
%strong
- if @user.last_sign_in_at
- = @user.last_sign_in_at.stamp("Nov 12, 2031")
+ = @user.last_sign_in_at.to_s(:medium)
- else
never
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index a144c43be47..0044d779c31 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -4,12 +4,12 @@
.row
= form_tag ci_lint_path, method: :post do
.form-group
- = label_tag :content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap'
+ = label_tag(:content, 'Content of .gitlab-ci.yml', class: 'control-label text-nowrap')
.col-sm-12
- = text_area_tag :content, nil, class: 'form-control span1', rows: 7, require: true
+ = text_area_tag(:content, @content, class: 'form-control span1', rows: 7, require: true)
.col-sm-12
.pull-left.prepend-top-10
- = submit_tag 'Validate', class: 'btn btn-success submit-yml'
+ = submit_tag('Validate', class: 'btn btn-success submit-yml')
.row.prepend-top-20
.col-sm-12
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index f98fd9f06ba..dc76599b776 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,9 +1,9 @@
.hidden-xs
= render "events/event_last_push", event: @last_push
-.gray-content-block
+.nav-block
- if current_user
- .pull-right
+ .controls
= link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'btn rss-btn' do
%i.fa.fa-rss
= render 'shared/event_filter'
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 9f4be025bf2..b78e70ebc1e 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
%li{ class: ("active" unless params[:filter]) }
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 64bd356f546..6ca97a692b4 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
Your Groups
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f4a3e3162bf..5c4b58cd688 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,7 +1,7 @@
= content_for :flash_message do
= render 'shared/project_limit'
.top-area
- %ul.left-top-menu
+ %ul.nav-links
= nav_link(page: [dashboard_projects_path, root_path]) do
= link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index 0ae62d6f1b6..b25e8ea1f0c 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
= link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
Your Snippets
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 07bda1c77f8..0d7b1b30dc3 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/dashboard/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 4316c358dcb..3810267577c 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -16,7 +16,7 @@
- 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.
+ %span All issues for this milestone are closed. Navigate to the project to close the milestone.
.table-holder
%table.table
@@ -48,7 +48,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
-%ul.center-top-menu.no-top.no-bottom
+%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
diff --git a/app/views/dashboard/projects/index.atom.builder b/app/views/dashboard/projects/index.atom.builder
index c8c219f4cca..2e2712c5146 100644
--- a/app/views/dashboard/projects/index.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
xml.id dashboard_projects_url
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.xmlschema if @events.any?
@events.each do |event|
event_to_atom(xml, event)
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index 07b6d57932e..d4e7862981c 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -3,32 +3,36 @@
= render 'dashboard/snippets_head'
-.gray-content-block
- .pull-right
+.nav-block
+ .controls
= link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
= icon('plus')
New Snippet
- .btn-group.btn-group-next.snippet-scope-menu
- = link_to dashboard_snippets_path, class: "btn btn-default #{"active" unless params[:scope]}" do
- All
- %span.badge
- = current_user.snippets.count
-
- = link_to dashboard_snippets_path(scope: 'are_private'), class: "btn btn-default #{"active" if params[:scope] == "are_private"}" do
- Private
- %span.badge
- = current_user.snippets.are_private.count
-
- = link_to dashboard_snippets_path(scope: 'are_internal'), class: "btn btn-default #{"active" if params[:scope] == "are_internal"}" do
- Internal
- %span.badge
- = current_user.snippets.are_internal.count
-
- = link_to dashboard_snippets_path(scope: 'are_public'), class: "btn btn-default #{"active" if params[:scope] == "are_public"}" do
- Public
- %span.badge
- = current_user.snippets.are_public.count
+ .nav-links.snippet-scope-menu
+ %li{ class: ("active" unless params[:scope]) }
+ = link_to dashboard_snippets_path do
+ All
+ %span.badge
+ = current_user.snippets.count
+
+ %li{ class: ("active" if params[:scope] == "are_private") }
+ = link_to dashboard_snippets_path(scope: 'are_private') do
+ Private
+ %span.badge
+ = current_user.snippets.are_private.count
+
+ %li{ class: ("active" if params[:scope] == "are_internal") }
+ = link_to dashboard_snippets_path(scope: 'are_internal') do
+ Internal
+ %span.badge
+ = current_user.snippets.are_internal.count
+
+ %li{ class: ("active" if params[:scope] == "are_public") }
+ = link_to dashboard_snippets_path(scope: 'are_public') do
+ Public
+ %span.badge
+ = current_user.snippets.are_public.count
= render 'snippets/snippets'
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 41ad2c231d4..2c15e2c4891 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -7,7 +7,7 @@
%h3 Sign in
.login-body
- if form_based_providers.any?
- %ul.nav.nav-tabs
+ %ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 49fab016bfa..cb93ff2465e 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -19,7 +19,7 @@
.form-group.append-bottom-20#password-strength
= f.password_field :password, class: "form-control bottom", value: user[:password], id: "user_password_sign_up", placeholder: "Password", required: true
%div
- - if Gitlab.config.recaptcha.enabled
+ - if current_application_settings.recaptcha_enabled
= recaptcha_tags
%div
= f.submit "Sign up", class: "btn-create btn"
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/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index ffc37ad6178..abea86b026a 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -1,5 +1,5 @@
- if show_last_push_widget?(event)
- .gray-content-block.clear-block
+ .gray-content-block.clear-block.last-push-widget
.event-last-push
.event-last-push-text
%span You pushed to
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 8daac585960..e2f97fd9337 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,7 +1,7 @@
- header_title group_title(@group, "Settings", edit_group_path(@group))
- @blank_container = true
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
Group settings
.panel-body
@@ -24,15 +24,6 @@
%hr
= link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- .form-group
- %hr
- = f.label :public, class: 'control-label' do
- Public
- .col-sm-10
- .checkbox
- = f.check_box :public
- %span.descr Make this group public (even if there is no any public project inside this group)
-
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 3361d7e2a8d..e7ab4f2409b 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -4,7 +4,7 @@
.col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
- Search for existing users or invite new ones using their email address.
+ Search for users by name, username, or email, or invite new ones using their email address.
.form-group
= f.label :access_level, "Group Access", class: 'control-label'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 335bf036074..6a8acc42af9 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -2,7 +2,7 @@
- header_title group_title(@group, "Members", group_group_members_path(@group))
- @blank_container = true
-.group-members-page
+.group-members-page.prepend-top-default
- if current_user && current_user.can?(:admin_group_member, @group)
.panel.panel-default
.panel-heading
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 66fe7e25871..486d1d8587a 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml"
xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
xml.id issues_dashboard_url
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index d063b257b5e..1233da85524 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -54,7 +54,7 @@
#{@milestone.open_items_count} open
= milestone_progress_bar(@milestone)
-%ul.center-top-menu.no-top.no-bottom
+%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index f1d507a50c7..9ca11ed1177 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,7 +1,7 @@
- page_title "Projects"
- header_title group_title(@group, "Projects", projects_group_path(@group))
-.panel.panel-default
+.panel.panel-default.prepend-top-default
.panel-heading
%strong= @group.name
projects:
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index 7ea574434c3..5cc0f5e1d2e 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: group_url(@group, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: group_url(@group), rel: "alternate", type: "text/html"
xml.id group_url(@group)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.xmlschema 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 c2c7c581b3e..ebb3df7dca3 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
- unless can?(current_user, :read_group, @group)
- @disable_search_panel = true
@@ -6,6 +8,12 @@
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block
+ .cover-controls
+ - if @group && can?(current_user, :admin_group, @group)
+ = link_to icon('pencil'), edit_group_path(@group), class: 'btn'
+ - if current_user
+ = link_to icon('rss'), group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'btn rss-btn'
+
.avatar-holder
= link_to group_icon(@group), target: '_blank' do
= image_tag group_icon(@group), class: "avatar group-avatar s90"
@@ -19,8 +27,8 @@
.cover-desc.description
= markdown(@group.description, pipeline: :description)
-- if can?(current_user, :read_group, @group)
- %ul.center-top-menu.no-top
+
+ %ul.nav-links
%li.active
= link_to "#activity", 'data-toggle' => 'tab' do
Activity
@@ -29,23 +37,22 @@
= 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
+- if can?(current_user, :read_group, @group)
+ %div{ class: container_class }
+ .tab-content
+ .tab-pane.active#activity
+ .activity-filter-block
+ - if current_user
+ = render "events/event_last_push", event: @last_push
- = render 'shared/event_filter'
+ = render 'shared/event_filter'
- .content_list
- = spinner
+ .content_list
+ = spinner
- .tab-pane#projects
- = render "projects", projects: @projects
+ .tab-pane#projects
+ = render "projects", projects: @projects
- else
- %p
- This group does not have public projects
+ %p.nav-links.no-top
+ No projects to show
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index e8e331dd109..9ee6f07b26b 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -40,6 +40,32 @@
%td.shortcut
.key enter
%td Open Selection
+ %tr
+ %td.shortcut
+ .key t
+ %td Go to finding file
+ %tbody
+ %tr
+ %th
+ %th Finding Project File
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tr
+ %td.shortcut
+ .key esc
+ %td Go back
.col-lg-4
%table.shortcut-mappings
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d9ffda884c8..7b45bd09050 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -139,26 +139,9 @@
%h2#navs Navigation
%h4
- %code .center-top-menu
+ %code .nav-links
.example
- %ul.center-top-menu
- %li.active
- %a Open
- %li
- %a Closed
-
- %h4
- %code .btn-group.btn-group-next
- .example
- %div.btn-group.btn-group-next
- %a.btn.active Open
- %a.btn Closed
-
-
- %h4
- %code .nav.nav-tabs
- .example
- %ul.nav.nav-tabs
+ %ul.nav-links
%li.active
%a Open
%li
diff --git a/app/views/layouts/_broadcast.html.haml b/app/views/layouts/_broadcast.html.haml
index e7d477c225e..3a7e0929c16 100644
--- a/app/views/layouts/_broadcast.html.haml
+++ b/app/views/layouts/_broadcast.html.haml
@@ -1,4 +1 @@
-- if broadcast_message.present?
- .broadcast-message{ style: broadcast_styling(broadcast_message) }
- %i.fa.fa-bullhorn
- = broadcast_message.message
+= broadcast_message
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 2e0bd2007a3..38ca4f91c4d 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,13 +1,13 @@
+- page_description brand_title unless page_description
+
+- site_name = "GitLab"
%head{prefix: "og: http://ogp.me/ns#"}
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
- %meta{name: 'referrer', content: 'origin-when-cross-origin'}
-
- %meta{name: "description", content: page_description}
-# Open Graph - http://ogp.me/
%meta{property: 'og:type', content: "object"}
- %meta{property: 'og:site_name', content: "GitLab"}
+ %meta{property: 'og:site_name', content: site_name}
%meta{property: 'og:title', content: page_title}
%meta{property: 'og:description', content: page_description}
%meta{property: 'og:image', content: page_image}
@@ -20,8 +20,8 @@
%meta{property: 'twitter:image', content: page_image}
= page_card_meta_tags
- - page_title "GitLab"
- %title= page_title
+ %title= page_title(site_name)
+ %meta{name: "description", content: page_description}
= favicon_link_tag 'favicon.ico'
@@ -34,6 +34,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/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 035fe0056d3..96b38485425 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,4 +1,6 @@
- project = @target_project || @project
-:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
- GitLab.GfmAutoComplete.setup();
+
+- if @noteable
+ :javascript
+ GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
+ GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec7cd79bc54..26159989777 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -24,7 +24,7 @@
.content-wrapper
= render "layouts/flash"
= yield :flash_message
- %div{ class: container_class }
+ %div{ class: (container_class unless @no_container) }
.content
.clearfix
= yield
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 31888c5580e..2e483b7148d 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,5 +1,6 @@
-- page_title @group.name
-- header_title group_title(@group) unless header_title
-- sidebar "group" unless sidebar
+- page_title @group.name
+- page_description @group.description unless page_description
+- header_title group_title(@group) unless header_title
+- sidebar "group" unless sidebar
= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index c60ac5eefac..cffdb52cc23 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -29,13 +29,13 @@
= icon('cog fw')
%span
Runners
- %span.count= Ci::Runner.count(:all)
+ %span.count= number_with_delimiter(Ci::Runner.count(:all))
= nav_link path: 'builds#index' do
= link_to admin_builds_path do
= icon('link fw')
%span
Builds
- %span.count= Ci::Build.count(:all)
+ %span.count= number_with_delimiter(Ci::Build.count(:all))
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
= icon('file-text fw')
@@ -80,7 +80,7 @@
= 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' do
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index da698831300..106abd24a56 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -24,13 +24,13 @@
= 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' 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: 'Snippets' do
= icon('clipboard fw')
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 68da8d5de2a..e5e2a59eaed 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -25,14 +25,14 @@
%span
Issues
- if current_user
- %span.count= Issue.opened.of_group(@group).count
+ %span.count= number_with_delimiter(Issue.opened.of_group(@group).count)
= nav_link(path: 'groups#merge_requests') do
= link_to merge_requests_group_path(@group), title: 'Merge Requests' do
= icon('tasks fw')
%span
Merge Requests
- if current_user
- %span.count= MergeRequest.opened.of_group(@group).count
+ %span.count= number_with_delimiter(MergeRequest.opened.of_group(@group).count)
= nav_link(controller: [:group_members]) do
= link_to group_group_members_path(@group), title: 'Members' do
= icon('users fw')
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 64b30783c05..f3ded04419b 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -27,7 +27,7 @@
= icon('envelope-o fw')
%span
Emails
- %span.count= current_user.emails.count + 1
+ %span.count= number_with_delimiter(current_user.emails.count + 1)
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
= link_to edit_profile_password_path, title: 'Password' do
@@ -45,7 +45,7 @@
= icon('key fw')
%span
SSH Keys
- %span.count= current_user.keys.count
+ %span.count= number_with_delimiter(current_user.keys.count)
= nav_link(controller: :preferences) do
= link_to profile_preferences_path, title: 'Preferences' do
-# TODO (rspeicher): Better icon?
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index c0d62028639..270ccfd387f 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -25,7 +25,7 @@
%span
Activity
- if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree' do
= icon('files-o fw')
%span
@@ -44,7 +44,7 @@
= icon('cubes fw')
%span
Builds
- %span.count.builds_counter= @project.builds.running_or_pending.count(:all)
+ %span.count.builds_counter= number_with_delimiter(@project.builds.running_or_pending.count(:all))
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
@@ -67,7 +67,7 @@
%span
Issues
- if @project.default_issues_tracker?
- %span.count.issue_counter= @project.issues.opened.count
+ %span.count.issue_counter= number_with_delimiter(@project.issues.opened.count)
- if project_nav_tab? :merge_requests
= nav_link(controller: :merge_requests) do
@@ -75,7 +75,7 @@
= icon('tasks fw')
%span
Merge Requests
- %span.count.merge_counter= @project.merge_requests.opened.count
+ %span.count.merge_counter= number_with_delimiter(@project.merge_requests.opened.count)
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
@@ -117,4 +117,3 @@
%li.hidden
= link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network' do
Network
-
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 3ca4c340406..325c68c69dc 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -44,6 +44,10 @@
%br
-# 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.
+ If you'd like to receive fewer emails, you can
+ - if @sent_notification && @sent_notification.unsubscribable?
+ = link_to "unsubscribe", unsubscribe_sent_notification_url(@sent_notification)
+ from this thread or
+ adjust your notification settings.
- = email_action @target_url \ No newline at end of file
+ = email_action @target_url
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index abf73bcc709..ab527e8e438 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,6 +1,7 @@
-- page_title @project.name_with_namespace
-- header_title project_title(@project) unless header_title
-- sidebar "project" unless sidebar
+- page_title @project.name_with_namespace
+- page_description @project.description unless page_description
+- header_title project_title(@project) unless header_title
+- sidebar "project" unless sidebar
- content_for :scripts_body_top do
- project = @target_project || @project
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 4361f67a74d..3dd2595f1ad 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -17,7 +17,7 @@
%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")}
+ %i at #{commit.committed_date.to_s(:iso8601)}
%pre.commit-message
= commit.safe_message
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index aa0e263b6df..53869e36b28 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -8,7 +8,7 @@
\
= @message.reverse_compare? ? "Deleted commits:" : "Commits:"
- @message.commits.each do |commit|
- #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
+ #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.to_s(:iso8601)}
#{commit.safe_message}
\- - - - -
\
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 17e47c622ce..a42fd38de3a 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -6,7 +6,7 @@
.alert.alert-info
Some options are unavailable for LDAP accounts
-.account-page
+.account-page.prepend-top-default
.panel.panel-default.update-token
.panel-heading
Reset Private token
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 0ca8bd95157..3bd1f1af162 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -10,7 +10,7 @@
%strong= @key.title
%li
%span.light Created on:
- %strong= @key.created_at.stamp("Aug 21, 2011")
+ %strong= @key.created_at.to_s(:medium)
.col-md-8
%p
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 101880bd105..961b61d2e76 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,6 +1,6 @@
-.gray-content-block.activity-filter-block
+.nav-block.activity-filter-block
- if current_user
- .pull-right
+ .controls
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
%i.fa.fa-rss
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index fa978325ddd..96c2fa87f45 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,5 +1,5 @@
#tree-holder.tree-holder.clearfix
- .gray-content-block.second-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/_find_file_link.html.haml b/app/views/projects/_find_file_link.html.haml
new file mode 100644
index 00000000000..08e2fc48be7
--- /dev/null
+++ b/app/views/projects/_find_file_link.html.haml
@@ -0,0 +1,3 @@
+= link_to namespace_project_find_file_path(@project.namespace, @project, @ref), class: 'btn btn-grouped shortcuts-find-file', rel: 'nofollow' do
+ = icon('search')
+ %span Find File
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index e92115b9b98..298c6664997 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -18,26 +18,42 @@
= visibility_level_label(@project.visibility_level)
.cover-controls
- - if can?(current_user, :admin_project, @project)
- = link_to edit_project_path(@project), class: 'btn btn-gray' do
- = icon('pencil')
- if current_user
- &nbsp;
= link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), class: 'btn btn-gray' do
= icon('rss')
+ - access = user_max_access_in_project(current_user.id, @project)
+ - can_edit = can?(current_user, :admin_project, @project)
+ - if access || can_edit
+ %span.dropdown.project-settings-dropdown
+ %a.dropdown-new.btn.btn-gray#project-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right
+ - if can_edit
+ %li
+ = link_to edit_project_path(@project) do
+ Edit Project
+ - if access
+ %li
+ = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
+ data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project' do
+ Leave Project
.project-repo-buttons
.split-one.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
- = render "shared/clone_panel"
+ .clone-row
+ .project-clone-holder
+ = render "shared/clone_panel"
- .split-repo-buttons
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
+ .split-repo-buttons
+ .btn-group.pull-left
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
- = render 'projects/buttons/notifications'
+ = render 'projects/buttons/notifications'
-:coffeescript
- new Star() \ No newline at end of file
+:javascript
+ new Star();
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 54c818baaf4..1fb37ef6621 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,6 +1,6 @@
.md-area
.md-header.clearfix
- %ul.center-top-menu
+ %ul.nav-links
%li.active
%a.js-md-write-button(href="#md-write-holder" tabindex="-1")
Write
diff --git a/app/views/projects/_zen.html.haml b/app/views/projects/_zen.html.haml
index 7e6301abde8..e701253d7de 100644
--- a/app/views/projects/_zen.html.haml
+++ b/app/views/projects/_zen.html.haml
@@ -1,13 +1,12 @@
.zennable
- %input#zen-toggle-comment.zen-toggle-comment(tabindex="-1" type="checkbox")
.zen-backdrop
- - classes << ' js-gfm-input markdown-area'
+ - classes << ' js-gfm-input js-autosize markdown-area'
- if defined?(f) && f
- = f.text_area attr, class: classes, placeholder: ''
+ = f.text_area attr, class: classes
- else
- = text_area_tag attr, nil, class: classes, placeholder: ''
- %a.zen-enter-link(tabindex="-1" href="#")
+ = text_area_tag attr, nil, class: classes
+ %a.js-zen-enter(tabindex="-1" href="#")
= icon('expand')
Edit in fullscreen
- %a.zen-leave-link(href="#")
+ %a.js-zen-leave(tabindex="-1" href="#")
= icon('compress')
diff --git a/app/views/projects/artifacts/_tree_directory.html.haml b/app/views/projects/artifacts/_tree_directory.html.haml
new file mode 100644
index 00000000000..5b87d55efd5
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_directory.html.haml
@@ -0,0 +1,7 @@
+%tr{ class: 'tree-item' }
+ %td.tree-item-file-name
+ = tree_icon('folder', '755', directory.name)
+ %span.str-truncated
+ = link_to directory.name, browse_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: directory.path)
+ %td
+ %td
diff --git a/app/views/projects/artifacts/_tree_file.html.haml b/app/views/projects/artifacts/_tree_file.html.haml
new file mode 100644
index 00000000000..92c1648f726
--- /dev/null
+++ b/app/views/projects/artifacts/_tree_file.html.haml
@@ -0,0 +1,11 @@
+%tr{ class: 'tree-item' }
+ %td.tree-item-file-name
+ = tree_icon('file', '664', file.name)
+ %span.str-truncated
+ = file.name
+ %td
+ = number_to_human_size(file.metadata[:size], precision: 2)
+ %td
+ = link_to file_namespace_project_build_artifacts_path(@project.namespace, @project, @build, path: file.path),
+ class: 'btn btn-xs btn-default artifact-download' do
+ = icon('download')
diff --git a/app/views/projects/artifacts/browse.html.haml b/app/views/projects/artifacts/browse.html.haml
new file mode 100644
index 00000000000..1add7ef6bfb
--- /dev/null
+++ b/app/views/projects/artifacts/browse.html.haml
@@ -0,0 +1,24 @@
+- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
+= render 'projects/builds/header_title'
+
+#tree-holder.tree-holder
+ .gray-content-block.top-block.clearfix
+ .pull-right
+ = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, @build),
+ class: 'btn btn-default' do
+ = icon('download')
+ Download artifacts archive
+
+%div.tree-content-holder
+ .table-holder
+ %table.table.tree-table.table-striped
+ %thead
+ %tr
+ %th Name
+ %th Size
+ %th Download
+ = render partial: 'tree_directory', collection: @entry.directories(parent: true), as: :directory
+ = render partial: 'tree_file', collection: @entry.files, as: :file
+
+- if @entry.empty?
+ .center Empty
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 2a3315da3db..3d8d88834e2 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -1,4 +1,4 @@
-.gray-content-block.top-block
+.nav-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 09fa148b129..a279e6eda55 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -2,7 +2,7 @@
= render "header_title"
.file-editor
- %ul.center-top-menu.no-bottom.js-edit-mode
+ %ul.nav-links.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
= icon('edit')
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 5081bae6801..d276e5932d1 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,8 +1,12 @@
- commit = @repository.commit(branch.target)
+- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
+- diverging_commit_counts = @repository.diverging_commit_counts(branch)
+- number_commits_behind = diverging_commit_counts[:behind]
+- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
- %strong.str-truncated= branch.name
+ .branch-name.str-truncated= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
@@ -29,6 +33,17 @@
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has_tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
+ - if branch.name != @repository.root_ref
+ .divergence-graph{ title: "#{number_commits_ahead} commits ahead, #{number_commits_behind} commits behind #{@repository.root_ref}" }
+ .graph-side
+ .bar.bar-behind{ style: "width: #{number_commits_behind * bar_graph_width_factor}%" }
+ %span.count.count-behind= number_commits_behind
+ .graph-separator
+ .graph-side
+ .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
+ %span.count.count-ahead= number_commits_ahead
+
+
- if commit
= render 'projects/branches/commit', commit: commit, project: @project
- else
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 1a26908ab11..5d18c0d803a 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -8,9 +8,15 @@
- if @all_builds.running_or_pending.any?
= link_to 'Cancel running', cancel_all_namespace_project_builds_path(@project.namespace, @project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- %ul.center-top-menu
+ %ul.nav-links
%li{class: ('active' if @scope.nil?)}
= link_to project_builds_path(@project) do
+ All
+ %span.badge.js-totalbuilds-count
+ = number_with_delimiter(@all_builds.count(:id))
+
+ %li{class: ('active' if @scope == 'running')}
+ = link_to project_builds_path(@project, scope: :running) do
Running
%span.badge.js-running-count
= number_with_delimiter(@all_builds.running_or_pending.count(:id))
@@ -21,12 +27,6 @@
%span.badge.js-running-count
= number_with_delimiter(@all_builds.finished.count(:id))
- %li{class: ('active' if @scope == 'all')}
- = link_to project_builds_path(@project, scope: :all) do
- All
- %span.badge.js-totalbuilds-count
- = number_with_delimiter(@all_builds.count(:id))
-
.gray-content-block
#{(@scope || 'running').capitalize} builds from this project
@@ -40,7 +40,7 @@
%thead
%tr
%th Status
- %th Runner
+ %th Build ID
%th Commit
%th Ref
%th Stage
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index 5b7ecce86ab..2be572d3b10 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -14,7 +14,7 @@
#up-build-trace
- if @commit.matrix_for_ref?(@build.ref)
- %ul.center-top-menu.no-top.no-bottom
+ %ul.nav-links.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
@@ -89,9 +89,15 @@
Test coverage
%h1 #{@build.coverage}%
- - if current_user && can?(current_user, :download_build_artifacts, @project) && @build.download_url
- .build-widget.center
- = link_to "Download artifacts", @build.download_url, class: 'btn btn-sm btn-primary'
+ - if current_user && can?(current_user, :read_build_artifacts, @project) && @build.artifacts?
+
+ .build-widget.artifacts
+ %h4.title Build artifacts
+ .center
+ .btn-group{ role: :group }
+ = link_to "Download", @build.artifacts_download_url, class: 'btn btn-sm btn-primary'
+ - if @build.artifacts_browser_supported?
+ = link_to "Browse", @build.artifacts_browse_url, class: 'btn btn-sm btn-primary'
.build-widget
%h4.title
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index 1f639fecc30..511863d774e 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
- %span.dropdown
- %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ .btn-group
+ %a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
@@ -8,9 +8,10 @@
= link_to url_for_new_issue(@project, only_path: true) do
= icon('exclamation-circle fw')
New issue
- - if can?(current_user, :create_merge_request, @project)
+ - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
+ - if merge_project
%li
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project) do
+ = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project) do
= icon('tasks fw')
New merge request
- if can?(current_user, :create_snippet, @project)
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index f74f8b427ec..ea33aa472a6 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu.no-top.no-bottom.commit-ci-menu
+%ul.nav-links.no-top.no-bottom.commit-ci-menu
= nav_link(path: 'commit#show') do
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index ddb77fd796b..bbe820b8842 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -50,7 +50,7 @@
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
-.commit-box.gray-content-block.middle-block
+.commit-box.content-block
%h3.commit-title
= markdown escape_once(@commit.title), pipeline: :single_line
- if @commit.description.present?
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index 99d62503a94..7118a4846c6 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -1,6 +1,7 @@
- page_title "Builds", "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "projects/commits/header_title"
-= render "commit_box"
+.prepend-top-default
+ = render "commit_box"
= render "ci_menu"
= render "builds"
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 16ebce2d771..05dbe5ebea4 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,6 +1,10 @@
-- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+- page_description @commit.description
+
= render "projects/commits/header_title"
-= render "commit_box"
+
+.prepend-top-default
+ = render "commit_box"
- if @ci_commit
= render "ci_menu"
- else
diff --git a/app/views/projects/commit_statuses/_commit_status.html.haml b/app/views/projects/commit_statuses/_commit_status.html.haml
index 74a05df24d3..1736dccaf3c 100644
--- a/app/views/projects/commit_statuses/_commit_status.html.haml
+++ b/app/views/projects/commit_statuses/_commit_status.html.haml
@@ -66,8 +66,8 @@
%td
.pull-right
- - if current_user && can?(current_user, :download_build_artifacts, commit_status.project) && commit_status.download_url
- = link_to commit_status.download_url, title: 'Download artifacts' do
+ - if current_user && can?(current_user, :read_build_artifacts, commit_status.project) && commit_status.artifacts?
+ = link_to commit_status.artifacts_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?
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 28b82dd31f3..4d4b410ee29 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -5,13 +5,13 @@
- 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{ id: "commit-#{commit.short_id}" }
.commit-row-title
- %strong.str-truncated
+ .commit-title.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index 0cd9ce1f371..6c631228002 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -6,7 +6,7 @@
.col-md-2.hidden-xs.hidden-sm
%h5.commits-row-date
%i.fa.fa-calendar
- %span= day.stamp("28 Aug, 2010")
+ %span= day.strftime('%d %b, %Y')
.light
= pluralize(commits.count, 'commit')
.col-md-10.col-sm-12
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index fcccb002d7e..498c5e05b32 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(controller: [:commit, :commits]) do
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
diff --git a/app/views/projects/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 7ffa7317196..e310fafd82c 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -4,14 +4,14 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
- xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any?
+ xml.updated @commits.first.committed_date.xmlschema if @commits.any?
@commits.each do |commit|
xml.entry do
xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id)
xml.title truncate(commit.title, length: 80)
- xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
+ xml.updated commit.committed_date.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon(commit.author_email))
xml.author do |author|
xml.name commit.author_name
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 2dd99cc8215..ede64d47ab3 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -6,30 +6,34 @@
= render "head"
-.gray-content-block
+.gray-content-block.second-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
- .commits-feed-holder.hidden-xs.hidden-sm
+ .block-controls.hidden-xs.hidden-sm
- if create_mr_button?(@repository.root_ref, @ref)
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ .control
+ = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
+ = icon('plus')
+ Create Merge Request
+
+ .control
+ = form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'pull-left commits-search-form') do
+ = search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input', spellcheck: false }
- if current_user && current_user.private_token
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do
- = icon("rss")
+ .control
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'btn' do
+ = icon("rss")
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
%div{id: dom_id(@project)}
- #commits-list= render "commits", project: @project
+ #commits-list.content_list= render "commits", project: @project
.clear
= spinner
-- if @commits.count == @limit
- :javascript
- CommitsList.init("#{@ref}", #{@limit});
-
+:javascript
+ CommitsList.init("#{@ref}", #{@limit});
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index b2f9c14da88..d668f483bcb 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -3,7 +3,7 @@
- diff_files = safe_diff_files(diffs, diff_refs)
-.gray-content-block.middle-block.oneline-block
+.content-block.oneline-block
.inline-parallel-buttons
.btn-group
= inline_diff_btn
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 650629ef1b9..8a99aceef7f 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,6 +1,6 @@
- @blank_container = true
-.project-edit-container
+.project-edit-container.prepend-top-default
.project-edit-errors
.project-edit-content
.panel.panel-default
@@ -174,6 +174,19 @@
.danger-settings
+ .panel.panel-default
+ .panel-heading Housekeeping
+ .errors-holder
+ .panel-body
+ %p
+ Runs a number of housekeeping tasks within the current repository,
+ such as compressing file revisions and removing unreachable objects.
+ %br
+
+ .form-actions
+ = link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
+ method: :post, class: "btn btn-default"
+
- if can? current_user, :archive_project, @project
- if @project.archived?
.panel.panel-success
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 503d156661e..b34d106d565 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :flash_message do
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
@@ -17,40 +19,41 @@
file to this project.
- if can?(current_user, :download_code, @project)
- .prepend-top-20
- .empty_wrapper
- %h3.page-title-empty
- Command line instructions
- %div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{h git_user_name}"
- git config --global user.email "#{h git_user_email}"
+ %div{ class: container_class }
+ .prepend-top-20
+ .empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{h @project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
- - if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml
new file mode 100644
index 00000000000..40a2a61d8a1
--- /dev/null
+++ b/app/views/projects/find_file/show.html.haml
@@ -0,0 +1,27 @@
+- page_title "Find File", @ref
+- header_title project_title(@project, "Files", project_files_path(@project))
+
+.file-finder-holder.tree-holder.clearfix
+ .gray-content-block.top-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'find_file', path: @path
+ %ul.breadcrumb.repo-breadcrumb
+ %li
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref) do
+ = @project.path
+ %li.file-finder
+ %input#file_find.form-control.file-finder-input{type: "text", placeholder: 'Find by path'}
+
+ %div.tree-content-holder
+ .table-holder
+ %table.table.files-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %tbody
+ = spinner nil, true
+
+:javascript
+ var projectFindFile = new ProjectFindFile($(".file-finder-holder"), {
+ url: "#{escape_javascript(namespace_project_files_path(@project.namespace, @project, @ref, @options.merge(format: :json)))}",
+ treeUrl: "#{escape_javascript(namespace_project_tree_path(@project.namespace, @project, @ref))}",
+ blobUrlTemplate: "#{escape_javascript(namespace_project_blob_path(@project.namespace, @project, @id || @commit.id))}"
+ });
+ new ShortcutsFindFile(projectFindFile);
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index a47643bd09c..79a56647c53 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,4 +1,4 @@
-%ul.center-top-menu
+%ul.nav-links
= nav_link(action: :show) do
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index b18d9197d0b..a0511819c9f 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -47,14 +47,14 @@
= f.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created
+ This url will be triggered when an issue is created/updated/merged
%div
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created
+ This url will be triggered when a merge request is created/updated/merged
%div
= f.check_box :build_events, class: 'pull-left'
.prepend-left-20
diff --git a/app/views/projects/issues/_closed_by_box.html.haml b/app/views/projects/issues/_closed_by_box.html.haml
index de415ae51a4..38469ed4774 100644
--- a/app/views/projects/issues/_closed_by_box.html.haml
+++ b/app/views/projects/issues/_closed_by_box.html.haml
@@ -1,2 +1,4 @@
-.issue-closed-by-widget.gray-content-block.second-block.white
- This issue will be closed automatically when merge request #{markdown(merge_requests_sentence(@closed_by_merge_requests), pipeline: :gfm)} is accepted.
+.issue-closed-by-widget.second-block
+ - pluralized_mr_this = merge_request_count > 1 ? "these" : "this"
+ - pluralized_mr_is = merge_request_count > 1 ? "are" : "is"
+ When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically.
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index dc434cf38c4..673020a4e30 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,9 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- - if @issue.closed?
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- - else
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-nr btn-grouped btn-close js-note-target-close', title: 'Close Issue'
+ = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen Issue'
+ = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close Issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index ca5b1a8386d..e0e89b764d5 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -7,7 +7,7 @@
- if @issues.present?
.issuable-filter-count
%span.pull-right
- = @issues.total_count
+ = 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
index 254968e4f67..640a1962ffc 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -1,7 +1,7 @@
-if @merge_requests.any?
%h2.merge-requests-title
= pluralize(@merge_requests.count, 'Related Merge Request')
- %ul.bordered-list
+ %ul.unstyled-list
- has_any_ci = @merge_requests.any?(&:ci_commit)
- @merge_requests.each do |merge_request|
%li
@@ -11,7 +11,7 @@
- elsif has_any_ci
= icon('blank fw')
%span.merge-request-id
- \##{merge_request.iid}
+ \!#{merge_request.iid}
%span.merge-request-info
%strong
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
@@ -24,3 +24,5 @@
MERGED
- elsif merge_request.closed?
CLOSED
+ - if @closed_by_merge_requests.present?
+ = render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index dc8e477185b..ee8a9414657 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
- xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
+ xml.updated @issues.first.created_at.xmlschema if @issues.any?
@issues.each do |issue|
issue_to_atom(xml, issue)
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index f931a0d3b92..7ed898ce72f 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -53,9 +53,6 @@
.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
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index bff3c3b283d..1c7de94acfd 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,8 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
+ = link_to 'Close', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request"
- if @merge_request.closed?
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
+ = link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 0af970e4b92..29d09d0a652 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -7,7 +7,7 @@
- if @merge_requests.present?
.issuable-filter-count
%span.pull-right
- = @merge_requests.total_count
+ = 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_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index f2a12099b26..8b75976abd1 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,7 +18,7 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.commits-tab
= link_to url_for(params), data: {target: 'div#commits', action: 'commits', toggle: 'tab'} do
Commits
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ba7c2c01e93..200bfa5ac4f 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -38,14 +38,14 @@
= render "projects/merge_requests/show/how_to_merge"
= render "projects/merge_requests/widget/show.html.haml"
- - if @merge_request.open? && @merge_request.source_branch_exists? && @merge_request.can_be_merged? && @merge_request.can_be_merged_by?(current_user)
+ - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
.light.prepend-top-default
You can also accept this merge request manually using the
= succeed '.' do
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
- %ul.merge-request-tabs.center-top-menu.no-top.no-bottom
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: 'div#notes', action: 'notes', toggle: 'tab'} do
Discussion
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/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index d6a44c9f0a1..67d95ab0364 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -21,10 +21,11 @@
= render 'shared/milestone_expired', milestone: milestone
.col-sm-6
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
- %i.fa.fa-pencil-square-o
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs" do
+ = icon('pencil-square-o')
Edit
+ \
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
= link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
- %i.fa.fa-trash-o
+ = icon('trash-o')
Delete
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 1670ea8741a..1142c584592 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -20,16 +20,16 @@
.pull-right
- if can?(current_user, :admin_milestone, @project)
- if @milestone.active?
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-grouped"
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
- else
- = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-grouped"
+ = link_to 'Reopen Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :activate }), method: :put, class: "btn btn-reopen btn-nr btn-grouped"
- = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-remove" do
- %i.fa.fa-trash-o
+ = link_to namespace_project_milestone_path(@project.namespace, @project, @milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-grouped btn-nr btn-remove" do
+ = icon('trash-o')
Delete
- = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped" do
- %i.fa.fa-pencil-square-o
+ = link_to edit_namespace_project_milestone_path(@project.namespace, @project, @milestone), class: "btn btn-grouped btn-nr" do
+ = icon('pencil-square-o')
Edit
.detail-page-description.gray-content-block.second-block
@@ -57,7 +57,7 @@
%span.pull-right= @milestone.expires_at
= milestone_progress_bar(@milestone)
-%ul.center-top-menu.no-top.no-bottom
+%ul.nav-links.no-top.no-bottom
%li.active
= link_to '#tab-issues', 'data-toggle' => 'tab' do
Issues
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 3ccda1b381c..5d78652befa 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,5 +6,5 @@
= render 'projects/notes/hints'
.note-form-actions
- = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
- = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
+ = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
+ = link_to 'Cancel', '#', class: 'btn btn-nr btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index acb6dc52a8e..f10a4145d62 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -15,4 +15,4 @@
.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
+ %a.btn.btn-nr.btn-cancel.js-close-discussion-note-form Cancel
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/projects/notes/_notes.html.haml
index ca60dd239b2..62db86fb181 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/projects/notes/_notes.html.haml
@@ -2,10 +2,14 @@
- @discussions.each do |discussion_notes|
- note = discussion_notes.first
- if note_for_main_target?(note)
+ - next if note.cross_reference_not_visible_for?(current_user)
+
= render discussion_notes
- else
= render 'projects/notes/discussion', discussion_notes: discussion_notes
- else
- @notes.each do |note|
- next unless note.author
+ - next if note.cross_reference_not_visible_for?(current_user)
+
= render note
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index d708b01a114..f0f3bb3c177 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -4,7 +4,7 @@
.col-sm-10
= users_select_tag(:user_ids, multiple: true, class: 'input-large', scope: :all, email_user: true)
.help-block
- Search for existing users or invite new ones using their email address.
+ Search for users by name, username, or email, or invite new ones using their email address.
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 29225a36364..6239a148905 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -2,7 +2,7 @@
= render "header_title"
- @blank_container = true
-.project-members-page
+.project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 315afe4a764..2d5b9f43c24 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -1,5 +1,6 @@
- page_title "Runners"
-.light
+
+.light.prepend-top-default
%p
A 'runner' is a process which runs a build.
You can setup as many runners as you need.
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
index 15c49767556..d6762219108 100644
--- a/app/views/projects/show.atom.builder
+++ b/app/views/projects/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_url(@project.namespace, @project)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.xmlschema 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 7466a098e24..4310f038fc9 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,3 +1,5 @@
+- @no_container = true
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
@@ -8,11 +10,10 @@
= render 'shared/no_password'
= render 'projects/last_push'
-
= render "home_panel"
.project-stats.gray-content-block.second-block
- %ul.nav.nav-pills
+ %ul.nav
%li
= link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
@@ -57,26 +58,17 @@
= link_to add_contribution_guide_path(@project) do
Add Contribution guide
-- if @project.archived?
- .text-warning.center.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- Archived project! Repository is read-only
-
- if @repository.commit
.content-block.second-block.white
- = render 'projects/last_commit', commit: @repository.commit, project: @project
+ %div{ class: container_class }
+ = render 'projects/last_commit', commit: @repository.commit, project: @project
-%div{class: "project-show-#{default_project_view}"}
- = render default_project_view
+%div{ class: container_class }
+ - if @project.archived?
+ .text-warning.center.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ Archived project! Repository is read-only
-- if current_user
- - access = user_max_access_in_project(current_user.id, @project)
- - if access
- .prepend-top-20.project-footer
- .gray-content-block.footer-block.center
- You have #{access} access to this project.
- - if @project.project_member_by_id(current_user)
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
- Leave this project
+ %div{class: "project-show-#{default_project_view}"}
+ = render default_project_view
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 28b706c5c7e..56a7ced1236 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -3,7 +3,7 @@
%li
%div
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
- %strong
+ .tag-name
= icon('tag')
= tag.name
- if tag.message.present?
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index b594d4f1f27..dbb20347860 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -17,8 +17,8 @@
.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
+ .tag-name.title
+ = @tag.name
- if @tag.message.present?
%span.light
&nbsp;
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 1927883513a..558e6146ae9 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,6 +1,6 @@
%div.tree-content-holder
.table-holder
- %table.table#tree-slider{class: "table_#{@hex_path} tree-table table-striped" }
+ %table.table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
%th Name
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index ec14bd7f65a..91fb2a44594 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -3,15 +3,15 @@
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-
= render 'projects/last_push'
-- if can? current_user, :download_code, @project
- .tree-download-holder
- = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group pull-right hidden-xs hidden-sm', split_button: true
+.tree-controls
+ = render 'projects/find_file_link'
+ - if can? current_user, :download_code, @project
+ = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
#tree-holder.tree-holder.clearfix
- .gray-content-block.top-block
+ .nav-block
= render 'projects/tree/tree_header', tree: @tree
= render 'projects/tree/tree_content', tree: @tree
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index e6e6ad5bc4b..69ba301e231 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -7,7 +7,7 @@
= render 'projects/wikis/new'
- %ul.center-top-menu
+ %ul.nav-links
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index f0547e9c057..53b37b1104e 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -5,12 +5,9 @@
%a.close{href: "#", "data-dismiss" => "modal"} ×
%h3.page-title New Wiki Page
.modal-body
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
- %p.hidden.text-danger{data: { error: "slug" }}
- The page slug is invalid. Please don't use characters other then: a-z 0-9 _ - and /
- %p.hint
- Please don't use spaces.
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project)
.form-actions
= link_to 'Create Page', '#', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 11c8c4f0eba..dd27ea2b11b 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -3,14 +3,12 @@
= render 'nav'
.gray-content-block
- .row
- .col-sm-6
- %h3.page-title.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .col-sm-6
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
.git-empty.prepend-top-default
%fieldset
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 481451edb23..2c3fca439f3 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-tabs.search-filter
+%ul.nav-links.search-filter
- if @project
%li{class: ("active" if @scope == 'blobs')}
= link_to search_filter_path(scope: 'blobs') do
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 2a38c98dcfc..d0e64537621 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,7 +1,7 @@
- if @search_results.empty?
= render partial: "search/results/empty"
- else
- %p.light
+ .gray-content-block
Search results for
%code
= @search_term
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index f4f3dcfc29f..215dbb3909e 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -1,5 +1,7 @@
- page_title @search_term
-= render 'search/form'
+
+.prepend-top-default
+ = render 'search/form'
- if @search_term
= render 'search/category'
= render 'search/results'
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 687a59c270f..faf7e49ed29 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,7 +1,7 @@
- project = project || @project
-.git-clone-holder
- .btn-group.clone-options
+.git-clone-holder.input-group
+ .input-group-btn
%a#clone-dropdown.clone-dropdown-btn.btn{href: '#', 'data-toggle' => 'dropdown'}
%span
= default_clone_protocol.upcase
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8495774accc..c38d9313dba 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-.btn-group.btn-group-next.event-filter
+%ul.nav-links.event-filter
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_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/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index cbdecda4fff..f77feeb79cd 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,5 +1,5 @@
.milestones-filters
- %ul.center-top-menu
+ %ul.nav-links
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
= link_to milestones_filter_path(state: 'opened') do
Open
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index 28d6f421fea..5a60ff5a5da 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -50,7 +50,7 @@
= form.label :issues_events, class: 'list-label' do
%strong Issues events
%p.light
- This url will be triggered when an issue is created
+ This url will be triggered when an issue is created/updated/merged
- if @service.supported_events.include?("merge_request")
%div
= form.check_box :merge_requests_events, class: 'pull-left'
@@ -58,7 +58,7 @@
= form.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events
%p.light
- This url will be triggered when a merge request is created
+ This url will be triggered when a merge request is created/updated/merged
- if @service.supported_events.include?("build")
%div
= form.check_box :build_events, class: 'pull-left'
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a54c5fa8c33..f4cfa29ae56 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -10,8 +10,7 @@
%i.fa.fa-sign-out
= image_tag group_icon(group), class: "avatar s46 hidden-xs"
- = link_to group, class: 'group-name' do
- %strong= group.name
+ = link_to group.name, group, class: 'group-name'
- if group_member
as
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index ac6c248ccf1..8d6f47b38ef 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,25 +1,29 @@
.issues-filters
.issues-state-filters
- %ul.center-top-menu
+ %ul.nav-links
+ - if defined?(type) && type == :merge_requests
+ - page_context_word = 'merge requests'
+ - else
+ - page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened') do
+ = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do
#{state_filters_text_for(:opened, @project)}
- if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged') do
+ = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do
#{state_filters_text_for(:merged, @project)}
%li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed') do
+ = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do
#{state_filters_text_for(:closed, @project)}
- else
%li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed') do
+ = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do
#{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all') do
+ = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do
#{state_filters_text_for(:all, @project)}
.issues-details-filters.gray-content-block
@@ -30,13 +34,13 @@
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: "Any Assignee", null_user: true, 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: "Any Author", first_user: true, current_user: true)
+ .filter-item.inline
+ = users_select_tag(:assignee_id, selected: params[:assignee_id],
+ placeholder: 'Assignee', class: 'trigger-submit', any_user: "Any Assignee", null_user: true, first_user: true, current_user: true)
+
.filter-item.inline.milestone-filter
= select_tag('milestone_title', projects_milestones_options,
class: 'select2 trigger-submit', include_blank: true,
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 79c5cc7f40a..9f4a7098ea2 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -54,14 +54,6 @@
= 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
@@ -78,6 +70,16 @@
.subscribed{class: ( 'hidden' unless subscribed )}
You're receiving notifications because you're subscribed to this thread.
+ - project_ref = cross_project_reference(@project, issuable)
+ .block
+ .title
+ .cross-project-reference
+ %span
+ Reference:
+ %cite{title: project_ref}
+ = project_ref
+ = clipboard_button(clipboard_text: project_ref)
+
:javascript
new Subscription("#{toggle_subscription_path(issuable)}");
- new IssuableContext();
+ new IssuableContext(); \ No newline at end of file
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index c36995b94d7..5db8056b77c 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -4,8 +4,12 @@
- skip_namespace = false unless local_assigns[:skip_namespace] == true
- css_class = '' unless local_assigns[:css_class]
- css_class += " no-description" unless project.description.present?
+- ci_commit = project.ci_commit(project.commit.sha) if ci && !project.empty_repo? && project.commit
+- cache_key = [project.namespace, project, controller.controller_name, controller.action_name, current_application_settings, 'v2.2']
+- cache_key.push(ci_commit.status) if ci_commit
+
%li.project-row{ class: css_class }
- = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do
+ = cache(cache_key) do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
@@ -19,10 +23,9 @@
= project.name
.project-controls
- - if ci && !project.empty_repo? && project.commit
- - if ci_commit = project.ci_commit(project.commit.sha)
- = render_ci_status(ci_commit)
- &nbsp;
+ - if ci_commit
+ = render_ci_status(ci_commit)
+ &nbsp;
- if stars
%span
%i.fa.fa-star
diff --git a/app/views/sherlock/queries/show.html.haml b/app/views/sherlock/queries/show.html.haml
index 4a84348ac82..83f61ce4b07 100644
--- a/app/views/sherlock/queries/show.html.haml
+++ b/app/views/sherlock/queries/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction'), t('sherlock.query')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/sherlock/transactions/show.html.haml b/app/views/sherlock/transactions/show.html.haml
index 3c8ffb06648..9d4b0b2724c 100644
--- a/app/views/sherlock/transactions/show.html.haml
+++ b/app/views/sherlock/transactions/show.html.haml
@@ -1,7 +1,7 @@
- page_title t('sherlock.title'), t('sherlock.transaction')
- header_title t('sherlock.title'), sherlock_transactions_path
-%ul.center-top-menu
+%ul.nav-links
%li.active
%a(href="#tab-general" data-toggle="tab")
= t('sherlock.general')
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 2fe5b7fac83..114d1e7a379 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -4,7 +4,7 @@ xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://sear
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
xml.id user_url(@user)
- xml.updated @events.latest_update_time.strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+ xml.updated @events.latest_update_time.xmlschema 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 0bca8177e14..849304ee2b6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,6 +1,7 @@
- page_title @user.name
- page_description @user.bio
- header_title @user.name, user_path(@user)
+- @no_container = true
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
@@ -8,6 +9,25 @@
= render 'shared/show_aside'
.cover-block
+ .cover-controls
+ - if @user == current_user
+ = link_to profile_path, class: 'btn btn-gray' do
+ = icon('pencil')
+ - elsif current_user
+ %span.report-abuse
+ - if @user.abuse_report
+ %button.btn.btn-danger{ title: 'Already reported for abuse',
+ data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
+ = icon('exclamation-circle')
+ - else
+ = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
+ title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
+ = icon('exclamation-circle')
+ - if current_user
+ &nbsp;
+ = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
+ = icon('rss')
+
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
@@ -21,7 +41,7 @@
%span
#{@user.bio}.
%span
- Member since #{@user.created_at.stamp("Aug 21, 2011")}
+ Member since #{@user.created_at.to_s(:medium)}
.cover-desc
- unless @user.public_email.blank?
@@ -47,74 +67,56 @@
= icon('map-marker')
= @user.location
+ %ul.nav-links.center
+ %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
- .cover-controls
- - if @user == current_user
- = link_to profile_path, class: 'btn btn-gray' do
- = icon('pencil')
- - elsif current_user
- %span.report-abuse
- - if @user.abuse_report
- %button.btn.btn-danger{ title: 'Already reported for abuse',
- data: { toggle: 'tooltip', placement: 'left', container: 'body' }}
- = icon('exclamation-circle')
- - else
- = link_to new_abuse_report_path(user_id: @user.id), class: 'btn btn-gray',
- title: 'Report abuse', data: {toggle: 'tooltip', placement: 'left', container: 'body'} do
- = icon('exclamation-circle')
- - if current_user
- &nbsp;
- = link_to user_path(@user, :atom, { private_token: current_user.private_token }), class: 'btn btn-gray' do
- = icon('rss')
-
-.gray-content-block.second-block
- .user-calendar
- %h4.center.light
- %i.fa.fa-spinner.fa-spin
- .user-calendar-activities
-
+%div{ class: container_class }
+ .tab-content
+ .tab-pane.active#activity
+ .gray-content-block.white.second-block
+ %div{ class: container_class }
+ .user-calendar
+ %h4.center.light
+ %i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
-%ul.center-top-menu.no-top.no-bottom.bottom-border.wide
- %li.active
- = link_to "#activity", 'data-toggle' => 'tab' do
- Activity
- - if @groups.any?
- %li
- = link_to "#groups", 'data-toggle' => 'tab' do
- Groups
- - if @contributed_projects.present?
- %li
- = link_to "#contributed", 'data-toggle' => 'tab' do
- Contributed projects
- - if @projects.present?
- %li
- = link_to "#personal", 'data-toggle' => 'tab' do
- Personal projects
-.tab-content
- .tab-pane.active#activity
- .content_list
- = spinner
+ .content_list
+ = spinner
- - if @groups.any?
- .tab-pane#groups
- %ul.content-list
- - @groups.each do |group|
- = render 'shared/groups/group', group: group
+ - 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 @contributed_projects.present?
+ .tab-pane#contributed
+ .contributed-projects
+ = render 'shared/projects/list',
+ projects: @contributed_projects.sort_by(&:star_count).reverse,
+ projects_limit: 10, 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
+ - 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 ce0a0113403..b1f8645eea0 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -20,27 +20,29 @@
= 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}
+ :javascript
+ var post_emoji_url = "#{award_toggle_namespace_project_notes_path(@project.namespace, @project)}";
+ var noteable_type = "#{votable.class.name.underscore}";
+ var noteable_id = "#{votable.id}";
+ var aliases = #{AwardEmoji.aliases.to_json};
window.awards_handler = new AwardsHandler(
post_emoji_url,
noteable_type,
noteable_id,
aliases
- )
+ );
- $(".awards").on "click", ".emoji-menu-content li", (e) ->
- emoji = $(this).find(".emoji-icon").data("emoji")
- awards_handler.addAward(emoji)
+ $(".awards").on("click", ".emoji-menu-content li", function(e) {
+ var 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)
+ $(".awards").on("click", ".award", function(e) {
+ var emoji = $(this).find(".icon").data("emoji");
+ awards_handler.addAward(emoji);
+ });
- $(".award").tooltip()
+ $(".award").tooltip();
- $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false})
+ $(".emoji-menu-content").niceScroll({cursorwidth: "7px", autohidemode: false});
diff --git a/app/workers/metrics_worker.rb b/app/workers/metrics_worker.rb
deleted file mode 100644
index b15dc819c5c..00000000000
--- a/app/workers/metrics_worker.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-class MetricsWorker
- include Sidekiq::Worker
-
- sidekiq_options queue: :metrics
-
- def perform(metrics)
- prepared = prepare_metrics(metrics)
-
- Gitlab::Metrics.pool.with do |connection|
- connection.write_points(prepared)
- end
- end
-
- def 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 escape_value(value)
- value.to_s.gsub('=', '\\=')
- end
-end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 84f0dfb64c8..d6e2c9380a5 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -204,6 +204,11 @@ production: &base
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
+ # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
+ # a request if the LDAP server becomes unresponsive.
+ # A value of 0 means there is no timeout.
+ timeout: 10
+
# This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false.
@@ -346,12 +351,6 @@ production: &base
# cas3:
# session_duration: 28800
- # reCAPTCHA settings. See: http://www.google.com/recaptcha
- recaptcha:
- enabled: false
- public_key: 'YOUR_PUBLIC_KEY'
- private_key: 'YOUR_PRIVATE_KEY'
-
# Shared file storage settings
shared:
# path: /mnt/gitlab # Default: shared
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 045bab739ea..d625a909bf1 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -11,7 +11,7 @@ class Settings < Settingslogic
# get host without www, thanks to http://stackoverflow.com/a/6674363/1233435
def get_host_without_www(url)
- url = URI.encode(url)
+ url = CGI.escape(url)
uri = URI.parse(url)
uri = URI.parse("http://#{url}") if uri.scheme.nil?
host = uri.host.downcase
@@ -108,6 +108,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
Settings.ldap['servers'].each do |key, server|
server['label'] ||= 'LDAP'
+ server['timeout'] ||= 10.seconds
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil?
server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil?
server['active_directory'] = true if server['active_directory'].nil?
@@ -131,12 +132,6 @@ Settings.omniauth.cas3['session_duration'] ||= 8.hours
Settings.omniauth['session_tickets'] ||= Settingslogic.new({})
Settings.omniauth.session_tickets['cas3'] = 'ticket'
-# ReCAPTCHA settings
-Settings['recaptcha'] ||= Settingslogic.new({})
-Settings.recaptcha['enabled'] = false if Settings.recaptcha['enabled'].nil?
-Settings.recaptcha['public_key'] ||= Settings.recaptcha['public_key']
-Settings.recaptcha['private_key'] ||= Settings.recaptcha['private_key']
-
Settings['shared'] ||= Settingslogic.new({})
Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root)
@@ -158,9 +153,9 @@ 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'
diff --git a/config/initializers/date_time_formats.rb b/config/initializers/date_time_formats.rb
new file mode 100644
index 00000000000..57568203cab
--- /dev/null
+++ b/config/initializers/date_time_formats.rb
@@ -0,0 +1,9 @@
+# :short - 10 Nov
+# :medium - Nov 10, 2007
+# :long - November 10, 2007
+Date::DATE_FORMATS[:medium] = '%b %-d, %Y'
+
+# :short - 18 Jan 06:10
+# :medium - Jan 18, 2007 6:10am
+# :long - January 18, 2007 06:10
+Time::DATE_FORMATS[:medium] = '%b %-d, %Y %-I:%M%P'
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 2e4908192a1..52ace27b7ae 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -1,6 +1,5 @@
if Gitlab::Metrics.enabled?
require 'influxdb'
- require 'socket'
require 'connection_pool'
require 'method_source'
diff --git a/config/initializers/recaptcha.rb b/config/initializers/recaptcha.rb
deleted file mode 100644
index 7509e327ae1..00000000000
--- a/config/initializers/recaptcha.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-if Gitlab.config.recaptcha.enabled
- Recaptcha.configure do |config|
- config.public_key = Gitlab.config.recaptcha['public_key']
- config.private_key = Gitlab.config.recaptcha['private_key']
- end
-end
diff --git a/config/routes.rb b/config/routes.rb
index 3e7d9f78710..75418db8d25 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -52,9 +52,6 @@ Rails.application.routes.draw do
API::API.logger Rails.logger
mount API::API => '/api'
- # Get all keys of user
- get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ }
-
constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
constraints constraint do
mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
@@ -91,6 +88,12 @@ Rails.application.routes.draw do
end
end
+ resources :sent_notifications, only: [], constraints: { id: /\h{32}/ } do
+ member do
+ get :unsubscribe
+ end
+ end
+
# Spam reports
resources :abuse_reports, only: [:new, :create]
@@ -222,7 +225,7 @@ Rails.application.routes.draw do
get :test
end
- resources :broadcast_messages, only: [:index, :create, :destroy]
+ resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy]
resource :logs, only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
@@ -378,6 +381,7 @@ Rails.application.routes.draw do
delete :remove_fork
post :archive
post :unarchive
+ post :housekeeping
post :toggle_star
post :markdown_preview
get :autocomplete_sources
@@ -441,6 +445,24 @@ Rails.application.routes.draw do
end
scope do
+ get(
+ '/find_file/*id',
+ to: 'find_file#show',
+ constraints: { id: /.+/, format: /html/ },
+ as: :find_file
+ )
+ end
+
+ scope do
+ get(
+ '/files/*id',
+ to: 'find_file#list',
+ constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+ as: :files
+ )
+ end
+
+ scope do
post(
'/create_dir/*id',
to: 'tree#create_dir',
@@ -497,7 +519,7 @@ Rails.application.routes.draw do
end
end
- WIKI_SLUG_ID = { id: /[a-zA-Z.0-9_\-\/]+/ } unless defined? WIKI_SLUG_ID
+ WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
@@ -588,9 +610,14 @@ Rails.application.routes.draw do
member do
get :status
post :cancel
- get :download
post :retry
end
+
+ resource :artifacts, only: [] do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
+ end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
@@ -668,5 +695,8 @@ Rails.application.routes.draw do
end
end
+ # Get all keys of user
+ get ':username.keys' => 'profiles/keys#get_keys' , constraints: { username: /.*/ }
+
get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
end
diff --git a/config/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/fixtures/development/14_builds.rb b/db/fixtures/development/14_builds.rb
new file mode 100644
index 00000000000..03a12323845
--- /dev/null
+++ b/db/fixtures/development/14_builds.rb
@@ -0,0 +1,79 @@
+class Gitlab::Seeder::Builds
+ BUILD_STATUSES = %w(running pending success failed canceled)
+
+ def initialize(project)
+ @project = project
+ end
+
+ def seed!
+ ci_commits.each do |ci_commit|
+ build = Ci::Build.new(build_attributes_for(ci_commit))
+
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
+
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
+
+ begin
+ build.save!
+ print '.'
+ rescue ActiveRecord::RecordInvalid
+ print 'F'
+ end
+ end
+ end
+
+ def ci_commits
+ commits = @project.repository.commits('master', nil, 5)
+ commits_sha = commits.map { |commit| commit.raw.id }
+ commits_sha.map do |sha|
+ @project.ensure_ci_commit(sha)
+ end
+ rescue
+ []
+ end
+
+ def build_attributes_for(ci_commit)
+ { name: 'test build', commands: "$ build command",
+ stage: 'test', stage_idx: 1, ref: 'master',
+ user_id: build_user, gl_project_id: @project.id,
+ status: build_status, commit_id: ci_commit.id,
+ created_at: Time.now, updated_at: Time.now }
+ end
+
+ def build_user
+ @project.team.users.sample
+ end
+
+ def build_status
+ BUILD_STATUSES.sample
+ end
+
+ def artifacts_archive_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ end
+
+ def artifacts_metadata_path
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+
+ end
+
+ def artifacts_cache_file(file_path)
+ cache_path = file_path.to_s.gsub('ci_', "p#{@project.id}_")
+
+ FileUtils.copy(file_path, cache_path)
+ File.open(cache_path) do |file|
+ yield file
+ end
+ end
+end
+
+Gitlab::Seeder.quiet do
+ Project.all.sample(10).each do |project|
+ project_builds = Gitlab::Seeder::Builds.new(project)
+ project_builds.seed!
+ end
+end
diff --git a/db/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/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/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
new file mode 100644
index 00000000000..6c282fc5039
--- /dev/null
+++ b/db/migrate/20151230132518_add_artifacts_metadata_to_ci_build.rb
@@ -0,0 +1,5 @@
+class AddArtifactsMetadataToCiBuild < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :artifacts_metadata, :text
+ end
+end
diff --git a/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
new file mode 100644
index 00000000000..78fdfeaf5cf
--- /dev/null
+++ b/db/migrate/20151231202530_remove_alert_type_from_broadcast_messages.rb
@@ -0,0 +1,5 @@
+class RemoveAlertTypeFromBroadcastMessages < ActiveRecord::Migration
+ def change
+ remove_column :broadcast_messages, :alert_type, :integer
+ end
+end
diff --git a/db/migrate/20160106162223_add_index_milestones_title.rb b/db/migrate/20160106162223_add_index_milestones_title.rb
new file mode 100644
index 00000000000..767885e2aac
--- /dev/null
+++ b/db/migrate/20160106162223_add_index_milestones_title.rb
@@ -0,0 +1,5 @@
+class AddIndexMilestonesTitle < ActiveRecord::Migration
+ def change
+ add_index :milestones, :title
+ end
+end
diff --git a/db/migrate/20160106164438_remove_influxdb_credentials.rb b/db/migrate/20160106164438_remove_influxdb_credentials.rb
new file mode 100644
index 00000000000..47e74400b97
--- /dev/null
+++ b/db/migrate/20160106164438_remove_influxdb_credentials.rb
@@ -0,0 +1,6 @@
+class RemoveInfluxdbCredentials < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :metrics_username, :string
+ remove_column :application_settings, :metrics_password, :string
+ end
+end
diff --git a/db/migrate/20160113111034_add_metrics_sample_interval.rb b/db/migrate/20160113111034_add_metrics_sample_interval.rb
new file mode 100644
index 00000000000..b741f5d2c75
--- /dev/null
+++ b/db/migrate/20160113111034_add_metrics_sample_interval.rb
@@ -0,0 +1,6 @@
+class AddMetricsSampleInterval < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_sample_interval, :integer,
+ default: 15
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index dc9ba36d0c7..2fc8c4d5ed4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20151228150906) do
+ActiveRecord::Schema.define(version: 20160113111034) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -33,33 +33,35 @@ ActiveRecord::Schema.define(version: 20151228150906) do
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.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.boolean "user_oauth_applications", default: true
t.string "after_sign_out_path"
- t.integer "session_expire_delay", default: 10080, null: false
+ t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
t.text "help_page_text"
t.string "admin_notification_email"
- t.boolean "shared_runners_enabled", default: true, null: false
- t.integer "max_artifacts_size", default: 100, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
+ t.integer "max_artifacts_size", default: 100, null: false
t.string "runners_registration_token"
- t.boolean "require_two_factor_authentication", default: false
- t.integer "two_factor_grace_period", default: 48
- t.boolean "metrics_enabled", default: false
- t.string "metrics_host", default: "localhost"
- t.string "metrics_database", default: "gitlab"
- 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 "require_two_factor_authentication", default: false
+ t.integer "two_factor_grace_period", default: 48
+ t.boolean "metrics_enabled", default: false
+ t.string "metrics_host", default: "localhost"
+ t.integer "metrics_pool_size", default: 16
+ t.integer "metrics_timeout", default: 10
+ t.integer "metrics_method_call_threshold", default: 10
+ t.boolean "recaptcha_enabled", default: false
+ t.string "recaptcha_site_key"
+ t.string "recaptcha_private_key"
+ t.integer "metrics_port", default: 8089
+ t.integer "metrics_sample_interval", default: 15
end
create_table "audit_events", force: :cascade do |t|
@@ -80,7 +82,6 @@ ActiveRecord::Schema.define(version: 20151228150906) do
t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
- t.integer "alert_type"
t.datetime "created_at"
t.datetime "updated_at"
t.string "color"
@@ -122,6 +123,7 @@ ActiveRecord::Schema.define(version: 20151228150906) do
t.string "description"
t.text "artifacts_file"
t.integer "gl_project_id"
+ t.text "artifacts_metadata"
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
@@ -542,24 +544,23 @@ ActiveRecord::Schema.define(version: 20151228150906) do
add_index "milestones", ["due_date"], name: "index_milestones_on_due_date", using: :btree
add_index "milestones", ["project_id", "iid"], name: "index_milestones_on_project_id_and_iid", unique: true, using: :btree
add_index "milestones", ["project_id"], name: "index_milestones_on_project_id", using: :btree
+ add_index "milestones", ["title"], name: "index_milestones_on_title", using: :btree
create_table "namespaces", force: :cascade do |t|
- t.string "name", null: false
- t.string "path", null: false
+ t.string "name", null: false
+ t.string "path", null: false
t.integer "owner_id"
t.datetime "created_at"
t.datetime "updated_at"
t.string "type"
- t.string "description", default: "", null: false
+ t.string "description", default: "", null: false
t.string "avatar"
- t.boolean "public", default: false
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
- add_index "namespaces", ["public"], name: "index_namespaces_on_public", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: :cascade do |t|
@@ -793,12 +794,12 @@ ActiveRecord::Schema.define(version: 20151228150906) do
add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
create_table "users", force: :cascade do |t|
- t.string "email", default: "", null: false
- t.string "encrypted_password", default: "", null: false
+ t.string "email", default: "", null: false
+ t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
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"
@@ -806,22 +807,22 @@ ActiveRecord::Schema.define(version: 20151228150906) do
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.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.integer "theme_id", default: 1, null: false
t.string "bio"
- t.integer "failed_attempts", default: 0
+ 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.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.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"
@@ -830,23 +831,23 @@ ActiveRecord::Schema.define(version: 20151228150906) do
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.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.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.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", default: "", null: false
+ t.integer "dashboard", default: 0
+ t.integer "project_view", default: 0
t.integer "consumed_timestep"
- t.integer "layout", default: 0
- t.boolean "hide_project_limit", default: false
+ t.integer "layout", default: 0
+ t.boolean "hide_project_limit", default: false
t.string "unlock_token"
t.datetime "otp_grace_period_started_at"
end
diff --git a/doc/README.md b/doc/README.md
index d82ff8b908b..7d4f84857e0 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -7,7 +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](migration/README.md) Convert a SVN repository to Git and GitLab
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
- [Permissions](permissions/permissions.md) Learn what each role in a project (guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
@@ -19,6 +19,7 @@
## CI Documentation
- [Quick Start](ci/quick_start/README.md)
+- [Enable or disable GitLab CI](ci/enable_or_disable_ci.md)
- [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
- [Configuring runner](ci/runners/README.md)
- [Configuring deployment](ci/deployment/README.md)
@@ -56,7 +57,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.
-- [Environmental Variables](administration/environmental_variables.md) to configure GitLab.
+- [Environment Variables](administration/environment_variables.md) to configure GitLab.
- [Operations](operations/README.md) Keeping GitLab up and running
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic web hook setup and the importing of projects.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
@@ -69,6 +70,8 @@
## Contributor documentation
+- [Documentation styleguide](development/doc_styleguide.md) Use this styleguide if you are
+ contributing to documentation.
- [Development](development/README.md) Explains the architecture and the guidelines for shell commands.
- [Legal](legal/README.md) Contributor license agreements.
- [Release](release/README.md) How to make the monthly and security releases.
diff --git a/doc/administration/enviroment_variables.md b/doc/administration/environment_variables.md
index d7f5cb7c21f..1eb3a74d304 100644
--- a/doc/administration/enviroment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -9,11 +9,14 @@ 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
+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
@@ -39,7 +42,12 @@ GITLAB_DATABASE_PASSWORD |
GITLAB_DATABASE_HOST | localhost
GITLAB_DATABASE_PORT | 5432
-## Other variables
+## 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 25a31b235cc..2fa177ff4dd 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,9 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
+- [Builds](builds.md)
+- [Build triggers](build_triggers.md)
+- [Build Variables](build_variables.md)
## Clients
diff --git a/doc/api/build_triggers.md b/doc/api/build_triggers.md
new file mode 100644
index 00000000000..4a12e962b62
--- /dev/null
+++ b/doc/api/build_triggers.md
@@ -0,0 +1,108 @@
+# Build triggers
+
+You can read more about [triggering builds through the API](../ci/triggers/README.md).
+
+## List project triggers
+
+Get a list of project's build triggers.
+
+```
+GET /projects/:id/triggers
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+```
+
+```json
+[
+ {
+ "created_at": "2015-12-23T16:24:34.716Z",
+ "deleted_at": null,
+ "last_used": "2016-01-04T15:41:21.986Z",
+ "token": "fbdb730c2fbdb095a0862dbd8ab88b",
+ "updated_at": "2015-12-23T16:24:34.716Z"
+ },
+ {
+ "created_at": "2015-12-23T16:25:56.760Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "7b9148c158980bbd9bcea92c17522d",
+ "updated_at": "2015-12-23T16:25:56.760Z"
+ }
+]
+```
+
+## Get trigger details
+
+Get details of project's build trigger.
+
+```
+GET /projects/:id/triggers/:token
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | The `token` of a trigger |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+```
+
+```json
+{
+ "created_at": "2015-12-23T16:25:56.760Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "7b9148c158980bbd9bcea92c17522d",
+ "updated_at": "2015-12-23T16:25:56.760Z"
+}
+```
+
+## Create a project trigger
+
+Create a build trigger for a project.
+
+```
+POST /projects/:id/triggers
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers"
+```
+
+```json
+{
+ "created_at": "2016-01-07T09:53:58.235Z",
+ "deleted_at": null,
+ "last_used": null,
+ "token": "6d056f63e50fe6f8c5f8f4aa10edb7",
+ "updated_at": "2016-01-07T09:53:58.235Z"
+}
+```
+
+## Remove a project trigger
+
+Remove a project's build trigger.
+
+```
+DELETE /projects/:id/triggers/:token
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|--------------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | The `token` of a project |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/triggers/7b9148c158980bbd9bcea92c17522d"
+```
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
new file mode 100644
index 00000000000..b96f1bdac8a
--- /dev/null
+++ b/doc/api/build_variables.md
@@ -0,0 +1,128 @@
+# Build Variables
+
+## List project variables
+
+Get list of a project's build variables.
+
+```
+GET /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+```
+
+```json
+[
+ {
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+ },
+ {
+ "key": "TEST_VARIABLE_2",
+ "value": "TEST_2"
+ }
+]
+```
+
+## Show variable details
+
+Get the details of a project's specific build variable.
+
+```
+GET /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+}
+```
+
+## Create variable
+
+Create a new build variable.
+
+```
+POST /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "new value"
+}
+```
+
+## Update variable
+
+Update a project's build variable.
+
+```
+PUT /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value"
+}
+```
+
+## Remove variable
+
+Remove a project's build variable.
+
+```
+DELETE /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+```
+
+```json
+{
+ "key": "VARIABLE_1",
+ "value": "VALUE_1"
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
new file mode 100644
index 00000000000..ecb50754c88
--- /dev/null
+++ b/doc/api/builds.md
@@ -0,0 +1,360 @@
+# Builds API
+
+## List project builds
+
+Get a list of builds in a project.
+
+```
+GET /projects/:id/builds
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+
+### Example of request
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds"
+```
+
+### Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.802Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:27.895Z",
+ "id": 7,
+ "name": "teaspoon",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:27.722Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.727Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:24.921Z",
+ "id": 6,
+ "name": "spinach:other",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:24.729Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## List commit builds
+
+Get a list of builds for specific commit in a project.
+
+```
+GET /projects/:id/repository/commits/:sha/builds
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| sha | string | yes | The SHA id of a commit |
+| scope | string|array of strings | no | The scope of builds to show, one or array of: `pending`, `running`, `failed`, `success`, `canceled`; showing all builds if none provided |
+
+### Example of request
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/repository/commits/0ff3ae198f8601a285adcf5c0fff204ee6fba5fd/builds"
+```
+
+### Example of response
+
+```json
+[
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "download_url": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+ },
+ {
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.957Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:33.913Z",
+ "id": 9,
+ "name": "brakeman",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:33.727Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+ }
+]
+```
+
+## Get a single build
+
+Get a single build of a project
+
+```
+GET /projects/:id/builds/:build_id
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| build\_id | integer | yes | The ID of a build |
+
+### Example of request
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/8"
+```
+
+### Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2015-12-24T15:51:21.880Z",
+ "download_url": null,
+ "finished_at": "2015-12-24T17:54:31.198Z",
+ "id": 8,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": "2015-12-24T17:54:30.733Z",
+ "status": "failed",
+ "tag": false,
+ "user": {
+ "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "bio": null,
+ "created_at": "2015-12-21T13:14:24.077Z",
+ "id": 1,
+ "is_admin": true,
+ "linkedin": "",
+ "name": "Administrator",
+ "skype": "",
+ "state": "active",
+ "twitter": "",
+ "username": "root",
+ "web_url": "http://gitlab.dev/u/root",
+ "website_url": ""
+ }
+}
+```
+
+## Cancel a build
+
+Cancel a single build of a project
+
+```
+POST /projects/:id/builds/:build_id/cancel
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| build\_id | integer | yes | The ID of a build |
+
+### Example of request
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/cancel"
+```
+
+### Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "download_url": null,
+ "finished_at": "2016-01-11T10:14:09.526Z",
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "canceled",
+ "tag": false,
+ "user": null
+}
+```
+
+## Retry a build
+
+Retry a single build of a project
+
+```
+POST /projects/:id/builds/:build_id/retry
+```
+
+### Parameters
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| id | integer | yes | The ID of a project |
+| build\_id | integer | yes | The ID of a build |
+
+### Example of request
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/builds/1/retry"
+```
+
+### Example of response
+
+```json
+{
+ "commit": {
+ "author_email": "admin@example.com",
+ "author_name": "Administrator",
+ "created_at": "2015-12-24T16:51:14.000+01:00",
+ "id": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "message": "Test the CI integration.",
+ "short_id": "0ff3ae19",
+ "title": "Test the CI integration."
+ },
+ "coverage": null,
+ "created_at": "2016-01-11T10:13:33.506Z",
+ "download_url": null,
+ "finished_at": null,
+ "id": 69,
+ "name": "rubocop",
+ "ref": "master",
+ "runner": null,
+ "stage": "test",
+ "started_at": null,
+ "status": "pending",
+ "tag": false,
+ "user": null
+}
+```
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 366a1f8abec..8bc0a67067a 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -4,8 +4,7 @@
Get all merge requests for this project.
The `state` parameter can be used to get only merge requests with a given state (`opened`, `closed`, or `merged`) or all of them (`all`).
-The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests. With GitLab 8.2 the return fields `upvotes` and
-`downvotes` are deprecated and always return `0`.
+The pagination parameters `page` and `per_page` can be used to restrict the list of merge requests.
```
GET /projects/:id/merge_requests
@@ -58,7 +57,7 @@ Parameters:
## Get single MR
-Shows information about a single merge request. With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and always return `0`.
+Shows information about a single merge request.
```
GET /projects/:id/merge_request/:merge_request_id
@@ -141,8 +140,6 @@ Parameters:
## Get single MR changes
Shows information about the merge request including its files and changes.
-With GitLab 8.2 the return fields `upvotes` and `downvotes` are deprecated and
-always return `0`.
```
GET /projects/:id/merge_request/:merge_request_id/changes
@@ -213,9 +210,7 @@ Parameters:
## Create MR
-Creates a new merge request. With GitLab 8.2 the return fields `upvotes` and `
-downvotes` are deprecated and always return `0`.
-
+Creates a new merge request.
```
POST /projects/:id/merge_requests
```
@@ -266,8 +261,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Update MR
-Updates an existing merge request. You can change the target branch, title, or even close the MR. With GitLab 8.2 the return fields `upvotes` and `downvotes`
-are deprecated and always return `0`.
+Updates an existing merge request. You can change the target branch, title, or even close the MR.
```
PUT /projects/:id/merge_request/:merge_request_id
@@ -318,8 +312,7 @@ If an error occurs, an error number and a message explaining the reason is retur
## Accept MR
-Merge changes submitted with MR using this API. With GitLab 8.2 the return
-fields `upvotes` and `downvotes` are deprecated and always return `0`.
+Merge changes submitted with MR using this API.
If merge success you get `200 OK`.
diff --git a/doc/api/notes.md b/doc/api/notes.md
index 4d7ef288df8..d4d63e825ab 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -6,8 +6,7 @@ Notes are comments on snippets, issues or merge requests.
### List project issue notes
-Gets a list of all notes for a single issue. With GitLab 8.2 the return fields
-`upvote` and `downvote` are deprecated and always return `false`.
+Gets a list of all notes for a single issue.
```
GET /projects/:id/issues/:issue_id/notes
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0ca81ffd49e..241229221db 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -76,7 +76,10 @@ Parameters:
"updated_at": "2013-09-30T13: 46: 02Z"
},
"archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png"
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0
},
{
"id": 6,
@@ -129,7 +132,11 @@ Parameters:
}
},
"archived": false,
- "avatar_url": null
+ "avatar_url": null,
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02"
}
]
```
@@ -244,7 +251,11 @@ Parameters:
}
},
"archived": false,
- "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png"
+ "avatar_url": "http://example.com/uploads/project/avatar/3/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b"
}
```
@@ -482,6 +493,34 @@ Parameters:
- `id` (required) - The ID of a project
+## Uploads
+
+### Upload a file
+
+Uploads a file to the specified project to be used in an issue or merge request description, or a comment.
+
+```
+POST /projects/:id/uploads
+```
+
+Parameters:
+
+- `id` (required) - The ID of the project
+- `file` (required) - The file to be uploaded
+
+```json
+{
+ "alt": "dk",
+ "url": "/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png",
+ "is_image": true,
+ "markdown": "![dk](/uploads/66dbcd21ec5d24ed6ea225176098d52b/dk.png)"
+}
+```
+
+**Note**: The returned `url` is relative to the project path.
+In Markdown contexts, the link is automatically expanded when the format in `markdown` is used.
+
+
## Team members
### List project team members
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 085d387e824..17d12e9cc62 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -83,6 +83,26 @@ it will contain the annotation.
It returns 200 if the operation succeed. In case of an error,
405 with an explaining error message is returned.
+## Delete a tag
+
+Deletes a tag of a repository with given name. On success, this API method
+returns 200 with the name of the deleted tag. If the tag does not exist, the
+API returns 404.
+
+```
+DELETE /projects/:id/repository/tags/:tag_name
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+
+```json
+{
+ "tag_name": "v4.3.0"
+}
+```
## Create a new release
diff --git a/doc/api/users.md b/doc/api/users.md
index 66d2fd52526..b7fc903825e 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -123,6 +123,13 @@ Parameters:
"name": "John Smith",
"state": "active",
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
+ "created_at": "2012-05-23T08:00:58Z",
+ "is_admin": false,
+ "bio": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": ""
}
```
@@ -551,7 +558,8 @@ Parameters:
- `uid` (required) - id of specified user
-Will return `200 OK` on success, or `404 User Not Found` is user cannot be found.
+Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+`403 Forbidden` when trying to block an already blocked user by LDAP synchronization.
## Unblock user
@@ -565,4 +573,5 @@ Parameters:
- `uid` (required) - id of specified user
-Will return `200 OK` on success, or `404 User Not Found` is user cannot be found.
+Will return `200 OK` on success, `404 User Not Found` is user cannot be found or
+`403 Forbidden` when trying to unblock a user blocked by LDAP synchronization.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index a1f5513d88e..4cdd2e1ad33 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -3,6 +3,7 @@
### User documentation
* [Quick Start](quick_start/README.md)
+* [Enable or disable GitLab CI](enable_or_disable_ci.md)
* [Configuring project (.gitlab-ci.yml)](yaml/README.md)
* [Configuring runner](runners/README.md)
* [Configuring deployment](deployment/README.md)
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index 31458d61674..63fe840b369 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -174,7 +174,7 @@ The alias hostname for the service is made from the image name following these
rules:
1. Everything after `:` is stripped
-2. Backslash (`/`) is replaced with double underscores (`__`)
+2. Slash (`/`) is replaced with double underscores (`__`)
## Configuring services
diff --git a/doc/ci/enable_or_disable_ci.md b/doc/ci/enable_or_disable_ci.md
new file mode 100644
index 00000000000..9bd2f5aff22
--- /dev/null
+++ b/doc/ci/enable_or_disable_ci.md
@@ -0,0 +1,70 @@
+## Enable or disable GitLab CI
+
+_To effectively use GitLab CI, you need a valid [`.gitlab-ci.yml`](yaml/README.md)
+file present at the root directory of your project and a
+[runner](runners/README.md) properly set up. You can read our
+[quick start guide](quick_start/README.md) to get you started._
+
+If you are using an external CI server like Jenkins or Drone CI, it is advised
+to disable GitLab CI in order to not have any conflicts with the commits status
+API.
+
+---
+
+As of GitLab 8.2, GitLab CI is mainly exposed via the `/builds` page of a
+project. Disabling GitLab CI in a project does not delete any previous builds.
+In fact, the `/builds` page can still be accessed, although it's hidden from
+the left sidebar menu.
+
+GitLab CI is enabled by default on new installations and can be disabled either
+individually under each project's settings, or site-wide by modifying the
+settings in `gitlab.yml` and `gitlab.rb` for source and Omnibus installations
+respectively.
+
+### Per-project user setting
+
+The setting to enable or disable GitLab CI can be found with the name **Builds**
+under the **Features** area of a project's settings along with **Issues**,
+**Merge Requests**, **Wiki** and **Snippets**. Select or deselect the checkbox
+and hit **Save** for the settings to take effect.
+
+![Features settings](img/features_settings.png)
+
+---
+
+### Site-wide administrator setting
+
+You can disable GitLab CI site-wide, by modifying the settings in `gitlab.yml`
+and `gitlab.rb` for source and Omnibus installations respectively.
+
+Two things to note:
+
+1. Disabling GitLab CI, will affect only newly-created projects. Projects that
+ had it enabled prior to this modification, will work as before.
+1. Even if you disable GitLab CI, users will still be able to enable it in the
+ project's settings.
+
+---
+
+For installations from source, open `gitlab.yml` with your editor and set
+`builds` to `false`:
+
+```yaml
+## Default project features settings
+default_projects_features:
+ issues: true
+ merge_requests: true
+ wiki: true
+ snippets: false
+ builds: false
+```
+
+Save the file and restart GitLab: `sudo service gitlab restart`.
+
+For Omnibus installations, edit `/etc/gitlab/gitlab.rb` and add the line:
+
+```
+gitlab-rails['gitlab_default_projects_features_builds'] = false
+```
+
+Save the file and reconfigure GitLab: `sudo gitlab-ctl reconfigure`.
diff --git a/doc/ci/img/features_settings.png b/doc/ci/img/features_settings.png
new file mode 100644
index 00000000000..17aba5d14d8
--- /dev/null
+++ b/doc/ci/img/features_settings.png
Binary files differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 68dcfe23ffb..295d953db11 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -62,8 +62,9 @@ Now simply register the runner as any runner:
sudo gitlab-runner register
```
-Note that you will have to enable `Allows shared runners` for each project
-that you want to make use of a shared runner. This is by default `off`.
+Shared runners are enabled by default as of GitLab 8.2, but can be disabled with the
+`DISABLE SHARED RUNNERS` button. Previous versions of GitLab defaulted shared runners to
+disabled.
## Registering a Specific Runner
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
new file mode 100644
index 00000000000..0bd32b78201
--- /dev/null
+++ b/doc/development/doc_styleguide.md
@@ -0,0 +1,231 @@
+# Documentation styleguide
+
+This styleguide recommends best practices to improve documentation and to keep
+it organized and easy to find.
+
+## Naming
+
+- When creating a new document and it has more than one word in its name,
+ make sure to use underscores instead of spaces or dashes (`-`). For example,
+ a proper naming would be `import_projects_from_github.md`. The same rule
+ applies to images.
+
+## Text
+
+- Split up long lines, this makes it much easier to review and edit. Only
+ double line breaks are shown as a full line break in [GitLab markdown][gfm].
+ 80-100 characters is a good line length
+- Make sure that the documentation is added in the correct directory and that
+ there's a link to it somewhere useful
+- Do not duplicate information
+- Be brief and clear
+- Unless there's a logical reason not to, add documents in alphabetical order
+- Write in US English
+- Use [single spaces][] instead of double spaces
+
+## Formatting
+
+- Use dashes (`-`) for unordered lists instead of asterisks (`*`)
+- Use the number one (`1`) for ordered lists
+- Use underscores (`_`) to mark a word or text in italics
+- Use double asterisks (`**`) to mark a word or text in bold
+- When using lists, prefer not to end each item with a period. You can use
+ them if there are multiple sentences, just keep the last sentence without
+ a period
+
+## Headings
+
+- Add only one H1 title in each document, by adding `#` at the beginning of
+ it (when using markdown). For subheadings, use `##`, `###` and so on
+- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
+ links shift too, which eventually leads to dead links. If you think it is
+ compelling to add numbers in headings, make sure to at least discuss it with
+ someone in the Merge Request
+- When introducing a new document, be careful for the headings to be
+ grammatically and syntactically correct. It is advised to mention one or all
+ of the following GitLab members for a review: `@axil`, `@rspeicher`,
+ `@dblessing`, `@ashleys`, `@nearlythere`. This is to ensure that no document
+ with wrong heading is going live without an audit, thus preventing dead links
+ and redirection issues when corrected
+- Leave exactly one newline after a heading
+
+## Links
+
+- If a link makes the paragraph to span across multiple lines, do not use
+ the regular Markdown approach: `[Text](https://example.com)`. Instead use
+ `[Text][identifier]` and at the very bottom of the document add:
+ `[identifier]: https://example.com`. This is another way to create Markdown
+ links which keeps the document clear and concise. Bonus points if you also
+ add an alternative text: `[identifier]: https://example.com "Alternative text"`
+ that appears when hovering your mouse on a link
+
+## Images
+
+- Place images in a separate directory named `img/` in the same directory where
+ the `.md` document that you're working on is located. Always prepend their
+ names with the name of the document that they will be included in. For
+ example, if there is a document called `twitter.md`, then a valid image name
+ could be `twitter_login_screen.png`.
+- Images should have a specific, non-generic name that will differentiate them.
+- Keep all file names in lower case.
+- Consider using PNG images instead of JPEG.
+
+Inside the document:
+
+- The Markdown way of using an image inside a document is:
+ `![Proper description what the image is about](img/document_image_title.png)`
+- Always use a proper description for what the image is about. That way, when a
+ browser fails to show the image, this text will be used as an alternative
+ description
+- If there are consecutive images with little text between them, always add
+ three dashes (`---`) between the image and the text to create a horizontal
+ line for better clarity
+- If a heading is placed right after an image, always add three dashes (`---`)
+ between the image and the heading
+
+## Notes
+
+- Notes should be in italics with the word `Note:` being bold. Use this form:
+ `_**Note:** This is something to note._`. If the note spans across multiple
+ lines it's OK to split the line.
+
+## New features
+
+- Every piece of documentation that comes with a new feature should declare the
+ GitLab version that feature got introduced. Right below the heading add a
+ note: `_**Note:** This feature was introduced in GitLab 8.3_`
+- If possible every feature should have a link to the MR that introduced it.
+ The above note would be then transformed to:
+ `_**Note:** This feature was [introduced][ce-1242] in GitLab 8.3_`, where
+ the [link identifier](#links) is named after the repository (CE) and the MR
+ number
+- If the feature is only in GitLab EE, don't forget to mention it, like:
+ `_**Note:** This feature was introduced in GitLab EE 8.3_`. Otherwise, leave
+ this mention out
+
+## API
+
+Here is a list of must-have items. Use them in the exact order that appears
+on this document. Further explanation is given below.
+
+- Every method must have the REST API request. For example:
+
+ ```
+ GET /projects/:id/repository/branches
+ ```
+
+- Every method must have a detailed
+ [description of the parameters](#method-description).
+- Every method must have a cURL example.
+- Every method must have a response body (in JSON format).
+
+### Method description
+
+Use the following table headers to describe the methods. Attributes should
+always be in code blocks using backticks (`).
+
+```
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+```
+
+Rendered example:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user` | string | yes | The GitLab username |
+
+### cURL commands
+
+- Use `https://gitlab.example.com/api/v3/` as an endpoint.
+- Wherever needed use this private token: `9koXpg98eAheJpvBs5tK`.
+- Always put the request first. `GET` is the default so you don't have to
+ include it.
+- Use double quotes to the URL when it includes additional parameters.
+- Prefer to use examples using the private token and don't pass data of
+ username and password.
+
+| Methods | Description |
+| ------- | ----------- |
+| `-H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK"` | Use this method as is, whenever authentication needed |
+| `-X POST` | Use this method when creating new objects |
+| `-X PUT` | Use this method when updating existing objects |
+| `-X DELETE` | Use this method when removing existing objects |
+
+### cURL Examples
+
+Below is a set of [cURL][] examples that you can use in the API documentation.
+
+#### Simple cURL command
+
+Get the details of a group:
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/gitlab-org
+```
+
+#### cURL example with parameters passed in the URL
+
+Create a new project under the authenticated user's namespace:
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects?name=foo"
+```
+
+#### Post data using cURL's --data
+
+Instead of using `-X POST` and appending the parameters to the URI, you can use
+cURL's `--data` option. The example below will create a new project `foo` under
+the authenticated user's namespace.
+
+```bash
+curl --data "name=foo" -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects"
+```
+
+#### Post data using JSON content
+
+_**Note:** In this example we create a new group. Watch carefully the single
+and double quotes._
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -H "Content-Type: application/json" --data '{"path": "my-group", "name": "My group"}' https://gitlab.example.com/api/v3/groups
+```
+
+#### Post data using form-data
+
+Instead of using JSON or urlencode you can use multipart/form-data which
+properly handles data encoding:
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -F "title=ssh-key" -F "key=ssh-rsa AAAAB3NzaC1yc2EA..." https://gitlab.example.com/api/v3/users/25/keys
+```
+
+The above example is run by and administrator and will add an SSH public key
+titled ssh-key to user's account which has an id of 25.
+
+#### Escape special characters
+
+Spaces or slashes (`/`) may sometimes result to errors, thus it is recommended
+to escape them when possible. In the example below we create a new issue which
+contains spaces in its title. Observe how spaces are escaped using the `%20`
+ASCII code.
+
+```bash
+curl -X POST -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/42/issues?title=Hello%20Dude"
+```
+
+Use `%2F` for slashes (`/`).
+
+#### Pass arrays to API calls
+
+The GitLab API sometimes accepts arrays of strings or integers. For example, to
+restrict the sign-up e-mail domains of a GitLab instance to `*.example.com` and
+`example.net`, you would do something like this:
+
+```bash
+curl -X PUT -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" -d "restricted_signup_domains[]=*.example.com" -d "restricted_signup_domains[]=example.net" https://gitlab.example.com/api/v3/application/settings
+```
+
+[cURL]: http://curl.haxx.se/ "cURL website"
+[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
+[gfm]: http://doc.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation"
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
index 86d205ba7a5..4cfb8402943 100644
--- a/doc/incoming_email/README.md
+++ b/doc/incoming_email/README.md
@@ -74,10 +74,11 @@ To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these
As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `incoming@gitlab.example.com`/`gitlab-incoming@gmail.com`.
-1. Reconfigure GitLab for the changes to take effect:
+1. Reconfigure GitLab and restart mailroom for the changes to take effect:
```sh
sudo gitlab-ctl reconfigure
+ sudo gitlab-ctl restart mailroom
```
1. Verify that everything is configured correctly:
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
index 18bf3db1744..787d21f7f8f 100644
--- a/doc/incoming_email/postfix.md
+++ b/doc/incoming_email/postfix.md
@@ -84,7 +84,12 @@ The instructions make the assumption that you will be using the email address `i
quit
```
- (Note: The `.` is a literal period on its own line)
+ _**Note:** The `.` is a literal period on its own line._
+
+ _**Note:** If you receive an error after entering `rcpt to: incoming@localhost`
+ then your Postfix `my_network` configuration is not correct. The error will
+ say 'Temporary lookup failure'. See
+ [Configure Postfix to receive email from the Internet](#configure-postfix-to-receive-email-from-the-internet)._
1. Check if the `incoming` user received the email:
@@ -131,7 +136,7 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
1. Test the new setup:
1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
- 2. Check if the `incoming` user received the email:
+ 1. Check if the `incoming` user received the email:
```sh
su - incoming
@@ -152,6 +157,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
q
```
+ _**Note:** If `mail` returns an error `Maildir: Is a directory` then your
+ version of `mail` doesn't support Maildir style mailboxes. Install
+ `heirloom-mailx` by running `sudo apt-get install heirloom-mailx`. Then,
+ try the above steps again, substituting `heirloom-mailx` for the `mail`
+ command._
+
1. Log out of the `incoming` account and go back to being `root`:
```sh
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 81edd8da2b8..00030729a4b 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -135,11 +135,11 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
- curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
- echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \
- sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz
+ curl -O --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
+ echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -c - && \
+ sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
- rm go1.5.1.linux-amd64.tar.gz
+ rm go1.5.3.linux-amd64.tar.gz
## 4. System Users
@@ -231,9 +231,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-3-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-4-stable gitlab
-**Note:** You can change `8-3-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-4-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -348,7 +348,7 @@ GitLab Shell is an SSH access and repository management software developed speci
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout 0.5.1
+ sudo -u git -H git checkout 0.5.4
sudo -u git -H make
### Initialize Database and Activate Advanced Features
@@ -552,6 +552,6 @@ Apart from the always supported markdown style there are other rich text files t
If you see this message when attempting to clone a repository hosted by GitLab,
this is likely due to an outdated Nginx or Apache configuration, or a missing or
-misconfigured `gitlab-git-http-server` instance. Double-check that you've
-[installed Go](#3-go), [installed gitlab-git-http-server](#install-gitlab-git-http-server),
+misconfigured gitlab-workhorse instance. Double-check that you've
+[installed Go](#3-go), [installed gitlab-workhorse](#install-gitlab-workhorse),
and correctly [configured Nginx](#site-configuration).
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 2a9f76533b7..5edac746c7b 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -1,6 +1,7 @@
# GitLab Integration
-GitLab integrates with multiple third-party services to allow external issue trackers and external authentication.
+GitLab integrates with multiple third-party services to allow external issue
+trackers and external authentication.
See the documentation below for details on how to configure these services.
@@ -15,13 +16,25 @@ See the documentation below for details on how to configure these services.
- [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).
+GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
## Project services
-Integration with services such as Campfire, Flowdock, Gemnasium, HipChat, Pivotal Tracker, and Slack are available in the form of a Project Service.
-You can find these within GitLab in the Services page under Project Settings if you are at least a master on the project.
-Project Services are a bit like plugins in that they allow a lot of freedom in adding functionality to GitLab, for example there is also a service that can send an email every time someone pushes new commits.
-Because GitLab is open source we can ship with the code and tests for all plugins.
-This allows the community to keep the plugins up to date so that they always work in newer GitLab versions.
-For an overview of what projects services are available without logging in please see the [project_services directory](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services).
+Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
+Pivotal Tracker, and Slack are available in the form of a [Project Service][].
+You can find these within GitLab in the Services page under Project Settings if
+you are at least a master on the project.
+Project Services are a bit like plugins in that they allow a lot of freedom in
+adding functionality to GitLab. For example there is also a service that can
+send an email every time someone pushes new commits.
+
+Because GitLab is open source we can ship with the code and tests for all
+plugins. This allows the community to keep the plugins up to date so that they
+always work in newer GitLab versions.
+
+For an overview of what projects services are available without logging in,
+please see the [project_services directory][projects-code].
+
+[jenkins]: http://doc.gitlab.com/ee/integration/jenkins.html
+[Project Service]: ../project_services/project_services.md
+[projects-code]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/models/project_services
diff --git a/doc/integration/azure.md b/doc/integration/azure.md
new file mode 100644
index 00000000000..48dddf7df44
--- /dev/null
+++ b/doc/integration/azure.md
@@ -0,0 +1,83 @@
+# Microsoft Azure OAuth2 OmniAuth Provider
+
+To enable the Microsoft Azure OAuth2 OmniAuth provider you must register your application with Azure. Azure will generate a client ID and secret key for you to use.
+
+1. Sign in to the [Azure Management Portal](https://manage.windowsazure.com>).
+
+1. Select "Active Directory" on the left and choose the directory you want to use to register GitLab.
+
+1. Select "Applications" at the top bar and click the "Add" button the bottom.
+
+1. Select "Add an application my organization is developing".
+
+1. Provide the project information and click the "Next" button.
+ - Name: 'GitLab' works just fine here.
+ - Type: 'WEB APPLICATION AND/OR WEB API'
+
+1. On the "App properties" page enter the needed URI's and click the "Complete" button.
+ - SIGN-IN URL: Enter the URL of your GitLab installation (e.g 'https://gitlab.mycompany.com/')
+ - APP ID URI: Enter the endpoint URL for Microsoft to use, just has to be unique (e.g 'https://mycompany.onmicrosoft.com/gitlab')
+
+1. Select "Configure" in the top menu.
+
+1. Add a "Reply URL" pointing to the Azure OAuth callback of your GitLab installation (e.g. https://gitlab.mycompany.com/users/auth/azure_oauth2/callback).
+
+1. Create a "Client secret" by selecting a duration, the secret will be generated as soon as you click the "Save" button in the bottom menu..
+
+1. Note the "CLIENT ID" and the "CLIENT SECRET".
+
+1. Select "View endpoints" from the bottom menu.
+
+1. You will see lots of endpoint URLs in the form 'https://login.microsoftonline.com/TENANT ID/...', note down the TENANT ID part of one of those endpoints.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "azure_oauth2",
+ "args" => {
+ "client_id" => "CLIENT ID",
+ "client_secret" => "CLIENT SECRET",
+ "tenant_id" => "TENANT ID",
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```
+ - { name: 'azure_oauth2',
+ args: { client_id: "CLIENT ID",
+ client_secret: "CLIENT SECRET",
+ tenant_id: "TENANT ID" } }
+ ```
+
+1. Replace 'CLIENT ID', 'CLIENT SECRET' and 'TENANT ID' with the values you got above.
+
+1. Save the configuration file.
+
+1. Restart GitLab for the changes to take effect.
+
+On the sign in page there should now be a Microsoft icon below the regular sign in form. Click the icon to begin the authentication process. Microsoft will ask the user to sign in and authorize the GitLab application. If everything goes well the user will be returned to GitLab and will be signed in.
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 3e660cfba1e..3543a67dd49 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -1,44 +1,30 @@
# External issue tracker
-GitLab has a great issue tracker but you can also use an external issue tracker such as Jira, Bugzilla or Redmine. You can configure issue trackers per GitLab project. For instance, if you configure Jira it allows you to do the following:
+GitLab has a great issue tracker but you can also use an external one such as
+Jira or Redmine. Issue trackers are configurable per GitLab project and allow
+you to do the following:
-- the 'Issues' link on the GitLab project pages takes you to the appropriate Jira issue index;
-- clicking 'New issue' on the project dashboard creates a new Jira issue;
-- To reference Jira issue PROJECT-1234 in comments, use syntax PROJECT-1234. Commit messages get turned into HTML links to the corresponding Jira issue.
-
-![Jira screenshot](jira-integration-points.png)
-
-GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html).
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ issue index of the external tracker
+- clicking **New issue** on the project dashboard creates a new issue on the
+ external tracker
## Configuration
-### Project Service
+The configuration is done via a project's **Services**.
-You can enable an external issue tracker per project. As an example, we will configure `Redmine` for project named gitlab-ci.
-
-Fill in the required details on the page:
+### Project Service
-![redmine configuration](redmine_configuration.png)
+To enable an external issue tracker you must configure the appropriate **Service**.
+Visit the links below for details:
-* `description` A name for the issue tracker (to differentiate between instances, for example).
-* `project_url` The URL to the project in Redmine which is being linked to this GitLab project.
-* `issues_url` The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the url. This id is used by GitLab as a placeholder to replace the issue number.
-* `new_issue_url` This is the URL to create a new issue in Redmine for the project linked to this GitLab project.
+- [Redmine](../project_services/redmine.md)
+- [Jira](jira.md)
### Service Template
-It is necessary to configure the external issue tracker per project, because project specific details are needed for the integration with GitLab.
-The admin can add a service template that sets a default for each project. This makes it much easier to configure individual projects.
-
-In GitLab Admin section, navigate to `Service Templates` and choose the service template you want to create:
-
-![redmine service template](redmine_service_template.png)
-
-After the template is created, the template details will be pre-filled on the project service page.
-
-NOTE: For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot
-with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically
-update the URL configured in `gitlab.yml`. This behavior is now depecated, and all issue tracker URLs must be configured directly
-within the project's Services settings.
+To save you the hassle from configuring each project's service individually,
+GitLab provides the ability to set Service Templates which can then be
+overridden in each project's settings.
-Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html).
+Read more on [Services Templates](../project_services/services_templates.md).
diff --git a/doc/integration/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png
index 15739a22dc7..15739a22dc7 100644
--- a/doc/integration/jira_issue_reference.png
+++ b/doc/integration/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png
new file mode 100644
index 00000000000..1e78daf105f
--- /dev/null
+++ b/doc/integration/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/integration/jira_project_name.png b/doc/integration/img/jira_project_name.png
index 5986fdb63fb..5986fdb63fb 100644
--- a/doc/integration/jira_project_name.png
+++ b/doc/integration/img/jira_project_name.png
Binary files differ
diff --git a/doc/integration/jira_service.png b/doc/integration/img/jira_service.png
index 1f6628c4371..1f6628c4371 100644
--- a/doc/integration/jira_service.png
+++ b/doc/integration/img/jira_service.png
Binary files differ
diff --git a/doc/integration/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png
index 67dfc6144c4..67dfc6144c4 100644
--- a/doc/integration/jira_service_close_issue.png
+++ b/doc/integration/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png
new file mode 100644
index 00000000000..2b37eda3520
--- /dev/null
+++ b/doc/integration/img/jira_service_page.png
Binary files differ
diff --git a/doc/integration/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png
index 8635a32eb68..8635a32eb68 100644
--- a/doc/integration/jira_workflow_screenshot.png
+++ b/doc/integration/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index 624601d0fac..de574d53410 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,14 +1,15 @@
# 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.
+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.
+Each project can be configured to connect to a different Jira instance, see the
+[configuration](#configuration) section. If you have one Jira instance you can
+pre-fill the settings page with a default template. To configure the template
+see the [Services Templates][services-templates] document.
+Once the project is connected to Jira, you can reference and close the issues
+in Jira directly from GitLab.
## Table of Contents
@@ -18,8 +19,11 @@ Once the project is connected to Jira, you can reference and close the issues in
### 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:
-
+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
@@ -29,85 +33,117 @@ When GitLab project has Jira issue tracker configured and enabled, mentioning Ji
* `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](img/jira_issue_reference.png)
-![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.
+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.
+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.
+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.
+Once this merge request is merged, the 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)
+![A Git commit that causes the Jira issue to be closed](img/jira_merge_request_close.png)
+---
-![The GitLab integration user leaves a comment on Jira](jira_service_close_issue.png)
+![The GitLab integration user leaves a comment on Jira](img/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.
+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
+JIRA configuration in GitLab is done via a project's **Services**.
-To enable JIRA integration in a project, navigate to the project Settings page and go to Services. Here you will find JIRA.
+#### GitLab 7.8 and up with JIRA v6.x
-Fill in the required details on the page:
+See next section.
-![Jira service page](jira_service_page.png)
+#### GitLab 7.8 and up
-* `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)
+_The currently supported JIRA versions are v6.x and v7.x._
-After saving the configuration, your GitLab project will be able to interact with the linked JIRA project.
+To enable JIRA integration in a project, navigate to the project's
+**Settings > Services > JIRA**.
+Fill in the required details on the page as described in the table below.
-### GitLab 6.x-7.7 with JIRA v6.x
+| Field | Description |
+| ----- | ----------- |
+| `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](img/jira_workflow_screenshot.png)). By default, this ID is `2` (in the example image, this is `2` as well) |
-**Note: GitLab 7.8 and up contain various integration improvements. We strongly recommend upgrading.**
+After saving the configuration, your GitLab project will be able to interact
+with the linked JIRA project.
+![Jira service page](img/jira_service_page.png)
-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.
+#### GitLab 6.x-7.7 with JIRA v6.x
-For example, we can close issues in JIRA by a commit in GitLab.
+_**Note:** GitLab versions 7.8 and up contain various integration improvements.
+We strongly recommend upgrading._
-Go to project settings page and fill in the project name for the JIRA project:
+In `gitlab.yml` enable the JIRA issue tracker section by
+[uncommenting these lines][jira-gitlab-yml]. This will make sure that all
+issues within GitLab are pointing to the JIRA issue tracker.
-![Set the JIRA project name in GitLab to 'NEW'](jira_project_name.png)
+After you set this, you will be able to close issues in JIRA by a commit in
+GitLab.
-Next, go to the services page and find JIRA.
+Go to your project's **Settings** page and fill in the project name for the
+JIRA project:
-![Jira services page](jira_service.png)
+![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.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`
+---
+
+You can also enable the JIRA service that will allow you to interact with JIRA
+issues. Go to the **Settings > Services > JIRA** and:
+
+1. Tick the active check box to enable the service
+1. Supply the URL to JIRA server, for example http://jira.example.com
+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
+1. Optional: supply the JIRA API version, default is version `2`
+1. Optional: supply the JIRA issue transition ID (issue transition to closed).
+ This is dependent on JIRA settings, default is `2`
+1. Hit save
+
+
+![Jira services page](img/jira_service.png)
-Now we should be able to interact with JIRA issues.
+[services-templates]: ../project_services/services_templates.md
+[jira-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115
diff --git a/doc/integration/jira_service_page.png b/doc/integration/jira_service_page.png
deleted file mode 100644
index 69ec44e826f..00000000000
--- a/doc/integration/jira_service_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 845f588f913..f256477196b 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -48,6 +48,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
bind_dn: '_the_full_dn_of_the_user_you_will_bind_with'
password: '_the_password_of_the_bind_user'
+ # Set a timeout, in seconds, for LDAP queries. This helps avoid blocking
+ # a request if the LDAP server becomes unresponsive.
+ # A value of 0 means there is no timeout.
+ timeout: 10
+
# This setting specifies if LDAP server is Active Directory LDAP server.
# For non AD servers it skips the AD specific queries.
# If your LDAP server is not AD, set this to false.
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index f2b1721fc03..e9e17eb4165 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -78,6 +78,7 @@ Now we can choose one or more of the Supported Providers below to continue confi
- [Shibboleth](shibboleth.md)
- [SAML](saml.md)
- [Crowd](crowd.md)
+- [Azure](azure.md)
## Enable OmniAuth for an Existing User
diff --git a/doc/integration/recaptcha.md b/doc/integration/recaptcha.md
index 7e6f7e7e30a..a301d1a613c 100644
--- a/doc/integration/recaptcha.md
+++ b/doc/integration/recaptcha.md
@@ -6,51 +6,18 @@ to confirm that a real user, not a bot, is attempting to create an account.
## Configuration
-To use reCAPTCHA, first you must create a public and private key.
+To use reCAPTCHA, first you must create a site and private key.
1. Go to the URL: https://www.google.com/recaptcha/admin
-1. Fill out the form necessary to obtain reCAPTCHA keys.
+2. Fill out the form necessary to obtain reCAPTCHA keys.
-1. On your GitLab server, open the configuration file.
+3. Login to your GitLab server, with administrator credentials.
- For omnibus package:
+4. Go to Applications Settings on Admin Area (`admin/application_settings`)
- ```sh
- sudo editor /etc/gitlab/gitlab.rb
- ```
+5. Fill all recaptcha fields with keys from previous steps
- For installations from source:
+6. Check the `Enable reCAPTCHA` checkbox
- ```sh
- cd /home/git/gitlab
-
- sudo -u git -H editor config/gitlab.yml
- ```
-
-1. Enable reCAPTCHA and add the settings:
-
- For omnibus package:
-
- ```ruby
- gitlab_rails['recaptcha_enabled'] = true
- gitlab_rails['recaptcha_public_key'] = 'YOUR_PUBLIC_KEY'
- gitlab_rails['recaptcha_private_key'] = 'YOUR_PUBLIC_KEY'
- ```
-
- For installation from source:
-
- ```
- recaptcha:
- enabled: true
- public_key: 'YOUR_PUBLIC_KEY'
- private_key: 'YOUR_PRIVATE_KEY'
- ```
-
-1. Change 'YOUR_PUBLIC_KEY' to the public key from step 2.
-
-1. Change 'YOUR_PRIVATE_KEY' to the private key from step 2.
-
-1. Save the configuration file.
-
-1. Restart GitLab.
+7. Save the configuration.
diff --git a/doc/integration/redmine_configuration.png b/doc/integration/redmine_configuration.png
deleted file mode 100644
index 6b145363229..00000000000
--- a/doc/integration/redmine_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/redmine_service_template.png b/doc/integration/redmine_service_template.png
deleted file mode 100644
index 1159eb5b964..00000000000
--- a/doc/integration/redmine_service_template.png
+++ /dev/null
Binary files differ
diff --git a/doc/integration/shibboleth.md b/doc/integration/shibboleth.md
index 6258e5f1030..a0be3dd4e5c 100644
--- a/doc/integration/shibboleth.md
+++ b/doc/integration/shibboleth.md
@@ -1,8 +1,8 @@
# Shibboleth OmniAuth Provider
-This documentation is for enabling shibboleth with gitlab-omnibus package.
+This documentation is for enabling shibboleth with omnibus-gitlab package.
-In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in gitlab-omnibus package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider.
+In order to enable Shibboleth support in gitlab we need to use Apache instead of Nginx (It may be possible to use Nginx, however I did not found way to easily configure Nginx that is bundled in omnibus-gitlab package). Apache uses mod_shib2 module for shibboleth authentication and can pass attributes as headers to omniauth-shibboleth provider.
To enable the Shibboleth OmniAuth provider you must:
diff --git a/doc/project_services/img/redmine_configuration.png b/doc/project_services/img/redmine_configuration.png
new file mode 100644
index 00000000000..d14e526ad33
--- /dev/null
+++ b/doc/project_services/img/redmine_configuration.png
Binary files differ
diff --git a/doc/project_services/img/services_templates_redmine_example.png b/doc/project_services/img/services_templates_redmine_example.png
new file mode 100644
index 00000000000..384d057fc8e
--- /dev/null
+++ b/doc/project_services/img/services_templates_redmine_example.png
Binary files differ
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 03937d20728..e3403127723 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -1,20 +1,37 @@
# Project Services
-
-__Project integrations with external services for continuous integration and more.__
+
+Project services allow you to integrate GitLab with other applications. Below
+is list of the currently supported ones. Click on the service links to see
+further configuration instructions and details. Contributions are welcome.
## Services
-- Assembla
-- [Atlassian Bamboo CI](bamboo.md) An Atlassian product for continuous integration.
-- Build box
-- Campfire
-- Emails on push
-- Flowdock
-- Gemnasium
-- GitLab CI
-- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging.
-- [Irker](irker.md) An IRC gateway to receive messages on repository updates.
-- Pivotal Tracker
-- Pushover
-- Slack
-- TeamCity
+| Service | Description |
+| ------- | ----------- |
+| Asana | Asana - Teamwork without email |
+| Assembla | Project Management Software (Source Commits Endpoint) |
+| [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
+| Buildkite | Continuous integration and deployments |
+| Builds emails | Email the builds status to a list of recipients |
+| Campfire | Simple web-based real-time group chat |
+| Custom Issue Tracker | Custom issue tracker |
+| Drone CI | Continuous Integration platform built on Docker, written in Go |
+| Emails on push | Email the commits and diff of each push to a list of recipients |
+| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
+| Flowdock | Flowdock is a collaboration web app for technical teams |
+| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
+| [HipChat](hipchat.md) | Private group chat and IM |
+| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
+| JIRA | Jira issue tracker |
+| JetBrains TeamCity CI | A continuous integration and build server |
+| PivotalTracker | Project Management Software (Source Commits Endpoint) |
+| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
+| [Redmine](redmine.md) | Redmine issue tracker |
+| Slack | A team communication tool for the 21st century |
+
+## Services Templates
+
+Services templates is a way to set some predefined values in the Service of
+your liking which will then be pre-filled on each project's Service.
+
+Read more about [Services Templates in this document](services_templates.md).
diff --git a/doc/project_services/redmine.md b/doc/project_services/redmine.md
new file mode 100644
index 00000000000..b9830ea7c38
--- /dev/null
+++ b/doc/project_services/redmine.md
@@ -0,0 +1,21 @@
+# Redmine Service
+
+Go to your project's **Settings > Services > Redmine** and fill in the required
+details as described in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `description` | A name for the issue tracker (to differentiate between instances, for example) |
+| `project_url` | The URL to the project in Redmine which is being linked to this GitLab project |
+| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
+| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project |
+
+Once you have configured and enabled Redmine:
+
+- the **Issues** link on the GitLab project pages takes you to the appropriate
+ Redmine issue index
+- clicking **New issue** on the project dashboard creates a new Redmine issue
+
+As an example, below is a configuration for a project named gitlab-ci.
+
+![Redmine configuration](img/redmine_configuration.png)
diff --git a/doc/project_services/services_templates.md b/doc/project_services/services_templates.md
new file mode 100644
index 00000000000..be6d13b6d2b
--- /dev/null
+++ b/doc/project_services/services_templates.md
@@ -0,0 +1,25 @@
+# Services Templates
+
+A GitLab administrator can add a service template that sets a default for each
+project. This makes it much easier to configure individual projects.
+
+After the template is created, the template details will be pre-filled on a
+project's Service page.
+
+## Enable a Service template
+
+In GitLab's Admin area, navigate to **Service Templates** and choose the
+service template you wish to create.
+
+For example, in the image below you can see Redmine.
+
+![Redmine service template](img/services_templates_redmine_example.png)
+
+---
+
+**NOTE:** For each project, you will still need to configure the issue tracking
+URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
+by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
+the project settings, and GitLab would automatically update the URL configured
+in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
+must be configured directly within the project's **Services** settings.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 093450a6de3..cdd6652b7b0 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -153,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.
diff --git a/doc/release/patch.md b/doc/release/patch.md
index 3022e375aca..1c921439156 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -24,7 +24,7 @@ Use the following template:
- Picked into respective `stable` branches:
- [ ] Merge `x-y-stable` into `x-y-stable-ee`
- [ ] release-tools: `x.y.z`
-- gitlab-omnibus
+- omnibus-gitlab
- [ ] `x.y.z+ee.0`
- [ ] `x.y.z+ce.0`
- [ ] Deploy
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index fe5b45dd432..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:**
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 5cb05b13b3e..612376e3a49 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -1,6 +1,6 @@
# System hooks
-Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
+Your GitLab instance can perform HTTP POST requests on the following events: `project_create`, `project_destroy`, `project_rename`, `project_transfer`, `user_add_to_team`, `user_remove_from_team`, `user_create`, `user_destroy`, `key_create`, `key_destroy`, `group_create`, `group_destroy`, `user_add_to_group` and `user_remove_from_group`.
System hooks can be used, e.g. for logging or changing information in a LDAP server.
@@ -17,6 +17,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -33,6 +34,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "project_destroy",
"name": "Underscore",
"owner_email": "johnsmith@gmail.com",
@@ -44,11 +46,48 @@ X-Gitlab-Event: System Hook
}
```
+**Project renamed:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_rename",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "jsmith/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
+**Project transferred:**
+
+```json
+{
+ "created_at": "2012-07-21T07:30:58Z",
+ "updated_at": "2012-07-21T07:38:22Z",
+ "event_name": "project_transfer",
+ "name": "Underscore",
+ "path": "underscore",
+ "path_with_namespace": "scores/underscore",
+ "project_id": 73,
+ "owner_name": "John Smith",
+ "owner_email": "johnsmith@gmail.com",
+ "project_visibility": "internal",
+ "old_path_with_namespace": "jsmith/overscore",
+}
+```
+
**New Team Member:**
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_team",
"project_access": "Master",
"project_id": 74,
@@ -57,6 +96,7 @@ X-Gitlab-Event: System Hook
"project_path_with_namespace": "jsmith/storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41,
"project_visibility": "private",
}
@@ -67,6 +107,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_team",
"project_access": "Master",
"project_id": 74,
@@ -75,6 +116,7 @@ X-Gitlab-Event: System Hook
"project_path_with_namespace": "jsmith/storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41,
"project_visibility": "private",
}
@@ -85,9 +127,11 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_create",
"name": "John Smith",
+ "username": "js",
"user_id": 41
}
```
@@ -97,9 +141,11 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:44:07Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"email": "js@gitlabhq.com",
"event_name": "user_destroy",
"name": "John Smith",
+ "username": "js",
"user_id": 41
}
```
@@ -110,6 +156,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_create",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -122,6 +169,7 @@ X-Gitlab-Event: System Hook
{
"event_name": "key_destroy",
"created_at": "2014-08-18 18:45:16 UTC",
+ "updated_at": "2012-07-21T07:38:22Z",
"username": "root",
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC58FwqHUbebw2SdT7SP4FxZ0w+lAO/erhy2ylhlcW/tZ3GY3mBu9VeeiSGoGz8hCx80Zrz+aQv28xfFfKlC8XQFpCWwsnWnQqO2Lv9bS8V1fIHgMxOHIt5Vs+9CAWGCCvUOAurjsUDoE2ALIXLDMKnJxcxD13XjWdK54j6ZXDB4syLF0C2PnAQSVY9X7MfCYwtuFmhQhKaBussAXpaVMRHltie3UYSBUUuZaB3J4cg/7TxlmxcNd+ppPRIpSZAB0NI6aOnqoBCpimscO/VpQRJMVLr3XiSYeT6HBiDXWHnIVPfQc03OGcaFqOit6p8lYKMaP/iUQLm+pgpZqrXZ9vB john@localhost",
"id": 4
@@ -133,6 +181,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_create",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -147,6 +196,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:54Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "group_destroy",
"name": "StoreCloud",
"owner_email": "johnsmith@gmail.com",
@@ -161,6 +211,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_add_to_group",
"group_access": "Master",
"group_id": 78,
@@ -168,6 +219,7 @@ X-Gitlab-Event: System Hook
"group_path": "storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41
}
```
@@ -176,6 +228,7 @@ X-Gitlab-Event: System Hook
```json
{
"created_at": "2012-07-21T07:30:56Z",
+ "updated_at": "2012-07-21T07:38:22Z",
"event_name": "user_remove_from_group",
"group_access": "Master",
"group_id": 78,
@@ -183,6 +236,7 @@ X-Gitlab-Event: System Hook
"group_path": "storecloud",
"user_email": "johnsmith@gmail.com",
"user_name": "John Smith",
+ "user_username": "johnsmith",
"user_id": 41
}
```
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 3748941b781..2ca4e1f3770 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -78,7 +78,7 @@ which should already be on your system from GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout 0.5.1
+sudo -u git -H git checkout 0.5.4
sudo -u git -H make
```
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
new file mode 100644
index 00000000000..1cbeab3eca3
--- /dev/null
+++ b/doc/update/8.3-to-8.4.md
@@ -0,0 +1,148 @@
+# From 8.3 to 8.4
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-4-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-4-stable-ee
+```
+
+### 4. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v2.6.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.4
+sudo -u git -H make
+```
+
+### 6. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+```
+
+### 7. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-3-stable:config/gitlab.yml.example origin/8-4-stable:config/gitlab.yml.example
+```
+
+#### Nginx configuration
+
+GitLab 8.3 introduced major changes in the NGINX configuration. Ensure you're
+still up-to-date with the latest changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-3-stable:lib/support/nginx/gitlab-ssl origin/8-4-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-3-stable:lib/support/nginx/gitlab origin/8-4-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-4-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. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 9. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.3)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.2 to 8.3](8.2-to-8.3.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index c19ee49f9e0..a10e62877ba 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -48,6 +48,7 @@ sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`ca
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch
sudo -u git -H git checkout `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION` -b `cat /home/git/gitlab/GITLAB_WORKHORSE_VERSION`
+sudo -u git -H make
```
### 5. Install libs, migrations, etc.
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index 8c9b4f72631..fffa0aba57f 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -1,25 +1,89 @@
# Project users
-You can manage the groups and users and their access levels in all of your projects. You can also personalize the access level you give each user, per project.
+You can manage the groups and users and their access levels in all of your
+projects. You can also personalize the access level you give each user,
+per-project.
-Here's how to add or import users to your projects.
-
-You should have 'master' or 'owner' permissions to add or import a new user
+You should have `master` or `owner` permissions to add or import a new user
to your project.
-To add or import a user, go to your project and click on "Members" on the left side of your screen:
+The first step to add or import a user, go to your project and click on
+**Members** on the left side of your screen.
+
+![Members](img/add_user_members_menu.png)
+
+---
+
+## Add a user
+
+Right next to **People**, start typing the name or username of the user you
+want to add.
+
+![Search for people](img/add_user_search_people.png)
+
+---
+
+Select the user and the [permission level](../../permissions/permissions.md)
+that you'd like to give the user. Note that you can select more than one user.
+
+![Give user permissions](img/add_user_give_permissions.png)
+
+---
+
+Once done, hit **Add users to project** and they will be immediately added to
+your project with the permissions you gave them above.
+
+![List members](img/add_user_list_members.png)
+
+---
+
+From there on, you can either remove an existing user or change their access
+level to the project.
+
+## Import users from another project
+
+You can import another project's users in your own project by hitting the
+**Import members** button on the upper right corner of the **Members** menu.
+
+In the dropdown menu, you can see only the projects you are Master on.
+
+![Import members from another project](img/add_user_import_members_from_another_project.png)
+
+---
+
+Select the one you want and hit **Import project members**. A flash message
+notifying you that the import was successful will appear, and the new members
+are now in the project's members list. Notice that the permissions that they
+had on the project you imported from are retained.
+
+![Members list of new members](img/add_user_imported_members.png)
+
+---
+
+## Invite people using their e-mail address
+
+If a user you want to give access to doesn't have an account on your GitLab
+instance, you can invite them just by typing their e-mail address in the
+user search field.
+
+![Invite user by mail](img/add_user_email_search.png)
+
+---
-![Members](images/members.png)
+As you can imagine, you can mix inviting multiple people and adding existing
+GitLab users to the project.
-Select "Add members" or "Import members" on the right side of your screen:
+![Invite user by mail ready to submit](img/add_user_email_ready.png)
-![Add or Import](images/add-members.png)
+---
-If you are adding a user, select the user and the [permission level](doc/permissions/permissions.md) that you'd like to
-give the user:
+Once done, hit **Add users to project** and watch that there is a new member
+with the e-mail address we used above. From there on, you can resend the
+invitation, change their access level or even delete them.
-![Add or Import](images/new-member.png)
+![Invite user members list](img/add_user_email_accept.png)
-If you are importing a user, follow the steps to select the project where you'd like to import the user from:
+---
-![Add or Import](images/select-project.png)
+Once the user accepts the invitation, they will be prompted to create a new
+GitLab account using the same e-mail address the invitation was sent to.
diff --git a/doc/workflow/add-user/images/add-members.png b/doc/workflow/add-user/images/add-members.png
deleted file mode 100644
index 2805c5764a5..00000000000
--- a/doc/workflow/add-user/images/add-members.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/add-user/images/new-member.png b/doc/workflow/add-user/images/new-member.png
deleted file mode 100644
index d500daea56e..00000000000
--- a/doc/workflow/add-user/images/new-member.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/add-user/images/select-project.png b/doc/workflow/add-user/images/select-project.png
deleted file mode 100644
index dd3844edff8..00000000000
--- a/doc/workflow/add-user/images/select-project.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/add-user/img/add_new_user_to_project_settings.png b/doc/workflow/add-user/img/add_new_user_to_project_settings.png
new file mode 100644
index 00000000000..3da18cdae53
--- /dev/null
+++ b/doc/workflow/add-user/img/add_new_user_to_project_settings.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png
new file mode 100644
index 00000000000..910affc9659
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_email_accept.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png
new file mode 100644
index 00000000000..5f02ce89b3e
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_email_ready.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png
new file mode 100644
index 00000000000..140979fbe13
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_email_search.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png
new file mode 100644
index 00000000000..8ef9156c8d5
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_give_permissions.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
new file mode 100644
index 00000000000..5770d5cf0c4
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png
new file mode 100644
index 00000000000..dea4b3f40ad
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_imported_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png
new file mode 100644
index 00000000000..7daa6ca7d9e
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/images/members.png b/doc/workflow/add-user/img/add_user_members_menu.png
index f1797b95f67..f1797b95f67 100644
--- a/doc/workflow/add-user/images/members.png
+++ b/doc/workflow/add-user/img/add_user_members_menu.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png
new file mode 100644
index 00000000000..5ac10ce80d4
--- /dev/null
+++ b/doc/workflow/add-user/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/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/shortcuts.png b/doc/workflow/shortcuts.png
index 68756ed1f98..e5914aa8e67 100644
--- a/doc/workflow/shortcuts.png
+++ b/doc/workflow/shortcuts.png
Binary files differ
diff --git a/doc_styleguide.md b/doc_styleguide.md
index cceb449a854..05ff46323ac 100644
--- a/doc_styleguide.md
+++ b/doc_styleguide.md
@@ -1,26 +1,3 @@
# Documentation styleguide
-This styleguide recommends best practices to improve documentation and to keep it organized and easy to find.
-
-## Text
-
-- Split up long lines, this makes it much easier to review and edit. Only
-double line breaks are shown as a full line break in markdown. 80 characters
-is a good line length.
-- For subtitles, make sure to start with the largest and go down, meaning:
-`#` for the title, `##` for subtitles and `###` for subtitles of the subtitles, etc.
-- Make sure that the documentation is added in the correct directory and that there's a link to it somewhere useful.
-- Add only one H1 or title in each document, by adding '#' at the begining of it (when using markdown).
-For subtitles, use '##', '###' and so on.
-- Do not duplicate information.
-- Be brief and clear.
-- Whenever it applies, add documents in alphabetical order.
-- Write in US English
-- Use [single spaces](http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html) instead of double spaces.
-
-## Images
-
-- Create a directory to store the images with the specific name of the document where the images belong.
-It could be in the same directory where the .md document that you're working on is located.
-- Images should have a specific, non-generic name that will differentiate them.
-- Keep all file names in lower case. \ No newline at end of file
+Moved to [development/doc_styleguide](doc/development/doc_styleguide.md).
diff --git a/features/admin/broadcast_messages.feature b/features/admin/broadcast_messages.feature
index b2c3112320a..fd3bac77f86 100644
--- a/features/admin/broadcast_messages.feature
+++ b/features/admin/broadcast_messages.feature
@@ -2,16 +2,11 @@
Feature: Admin Broadcast Messages
Background:
Given I sign in as an admin
- And application already has admin messages
+ And application already has a broadcast message
And I visit admin messages page
Scenario: See broadcast messages list
- Then I should be all broadcast messages
-
- Scenario: Create a broadcast message
- When submit form with new broadcast message
- Then I should be redirected to admin messages page
- And I should see newly created broadcast message
+ Then I should see all broadcast messages
Scenario: Create a customized broadcast message
When submit form with new customized broadcast message
@@ -19,3 +14,14 @@ Feature: Admin Broadcast Messages
And I should see newly created broadcast message
Then I visit dashboard page
And I should see a customized broadcast message
+
+ Scenario: Edit an existing broadcast message
+ When I edit an existing broadcast message
+ And I change the broadcast message text
+ Then I should be redirected to admin messages page
+ And I should see the updated broadcast message
+
+ Scenario: Remove an existing broadcast message
+ When I remove an existing broadcast message
+ Then I should be redirected to admin messages page
+ And I should not see the removed broadcast message
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/project/builds.feature b/features/project/builds.feature
new file mode 100644
index 00000000000..c00b0a7ae07
--- /dev/null
+++ b/features/project/builds.feature
@@ -0,0 +1,58 @@
+Feature: Project Builds
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And CI is enabled
+ And I have recent build for my project
+
+ Scenario: I browse build summary page
+ When I visit recent build summary page
+ Then I see summary for build
+ And I see build trace
+
+ Scenario: I download build artifacts
+ Given recent build has artifacts available
+ When I visit recent build summary page
+ And I click artifacts download button
+ Then download of build artifacts archive starts
+
+ Scenario: I browse build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build summary page
+ And I click artifacts browse button
+ Then I should see content of artifacts archive
+
+ Scenario: I browse subdirectory of build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I click link to subdirectory within build artifacts
+ Then I should see content of subdirectory within artifacts archive
+
+ Scenario: I browse directory with UTF-8 characters in name
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ And recent build artifacts contain directory with UTF-8 characters
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I navigate to directory with UTF-8 characters in name
+ Then I should see content of directory with UTF-8 characters in name
+
+ Scenario: I try to browse directory with invalid UTF-8 characters in name
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ And recent build artifacts contain directory with invalid UTF-8 characters
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I navigate to parent directory of directory with invalid name
+ Then I should not see directory with invalid name on the list
+
+ Scenario: I download a single file from build artifacts
+ Given recent build has artifacts available
+ And recent build has artifacts metadata available
+ When I visit recent build summary page
+ And I click artifacts browse button
+ And I click download button for a file within build artifacts
+ Then download of a file extracted from build artifacts should start
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 5bb2d0e976b..01c10721312 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -55,3 +55,8 @@ Feature: Project Commits
Scenario: I browse a commit with an image
Given I visit a commit with an image that changed
Then The diff links to both the previous and current image
+
+ @javascript
+ Scenario: I filter commits by message
+ When I search "submodules" commits
+ Then I should see only "submodules" commits
diff --git a/features/project/find_file.feature b/features/project/find_file.feature
new file mode 100644
index 00000000000..ae8fa245923
--- /dev/null
+++ b/features/project/find_file.feature
@@ -0,0 +1,42 @@
+@dashboard
+Feature: Project Find File
+ Background:
+ Given I sign in as a user
+ And I own a project
+ And I visit my project's files page
+
+ @javascript
+ Scenario: Navigate to find file by shortcut
+ Given I press "t"
+ Then I should see "find file" page
+
+ Scenario: Navigate to find file
+ Given I click Find File button
+ Then I should see "find file" page
+
+ @javascript
+ Scenario: I search file
+ Given I visit project find file page
+ And I fill in file find with "change"
+ Then I should not see ".gitignore" in files
+ And I should not see ".gitmodules" in files
+ And I should see "CHANGELOG" in files
+ And I should not see "VERSION" in files
+
+ @javascript
+ Scenario: I search file that not exist
+ Given I visit project find file page
+ And I fill in file find with "asdfghjklqwertyuizxcvbnm"
+ Then I should not see ".gitignore" in files
+ And I should not see ".gitmodules" in files
+ And I should not see "CHANGELOG" in files
+ And I should not see "VERSION" in files
+
+ @javascript
+ Scenario: I search file that partially matches
+ Given I visit project find file page
+ And I fill in file find with "git"
+ Then I should see ".gitignore" in files
+ And I should see ".gitmodules" in files
+ And I should not see "CHANGELOG" in files
+ And I should not see "VERSION" in files
diff --git a/features/project/fork.feature b/features/project/fork.feature
index 22f68e5b340..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/issues/references.feature b/features/project/issues/references.feature
new file mode 100644
index 00000000000..4ae2d653337
--- /dev/null
+++ b/features/project/issues/references.feature
@@ -0,0 +1,33 @@
+@project_issues
+Feature: Project Issues References
+ Background:
+ Given I sign in as "John Doe"
+ And public project "Community"
+ And "John Doe" owns public project "Community"
+ And project "Community" has "Community issue" open issue
+ And I logout
+ And I sign in as "Mary Jane"
+ And private project "Enterprise"
+ And "Mary Jane" owns private project "Enterprise"
+ And project "Enterprise" has "Enterprise issue" open issue
+ And project "Enterprise" has "Enterprise fix" open merge request
+ And I visit issue page "Enterprise issue"
+ And I leave a comment referencing issue "Community issue"
+ And I visit merge request page "Enterprise fix"
+ And I leave a comment referencing issue "Community issue"
+ And I logout
+
+ @javascript
+ Scenario: Viewing the public issue as a "John Doe"
+ Given I sign in as "John Doe"
+ When I visit issue page "Community issue"
+ Then I should not see any related merge requests
+ And I should see no notes at all
+
+ @javascript
+ Scenario: Viewing the public issue as "Mary Jane"
+ Given I sign in as "Mary Jane"
+ When I visit issue page "Community issue"
+ Then I should see the "Enterprise fix" related merge request
+ And I should see a note linking to "Enterprise fix" merge request
+ And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature
new file mode 100644
index 00000000000..571612261a9
--- /dev/null
+++ b/features/project/merge_requests/references.feature
@@ -0,0 +1,31 @@
+@project_merge_requests
+Feature: Project Merge Requests References
+ Background:
+ Given I sign in as "John Doe"
+ And public project "Community"
+ And "John Doe" owns public project "Community"
+ And project "Community" has "Community fix" open merge request
+ And I logout
+ And I sign in as "Mary Jane"
+ And private project "Enterprise"
+ And "Mary Jane" owns private project "Enterprise"
+ And project "Enterprise" has "Enterprise issue" open issue
+ And project "Enterprise" has "Enterprise fix" open merge request
+ And I visit issue page "Enterprise issue"
+ And I leave a comment referencing issue "Community fix"
+ And I visit merge request page "Enterprise fix"
+ And I leave a comment referencing issue "Community fix"
+ And I logout
+
+ @javascript
+ Scenario: Viewing the public issue as a "John Doe"
+ Given I sign in as "John Doe"
+ When I visit issue page "Community fix"
+ Then I should see no notes at all
+
+ @javascript
+ Scenario: Viewing the public issue as "Mary Jane"
+ Given I sign in as "Mary Jane"
+ When I visit issue page "Community fix"
+ And I should see a note linking to "Enterprise fix" merge request
+ And I should see a note linking to "Enterprise issue" issue
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index af970ecf2d0..d4811b1ff54 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -70,11 +70,6 @@ Feature: Project Wiki
Then I should see non-escaped link in the pages list
@javascript
- Scenario: Creating an invalid new page
- Given I create a New page with an invalid name
- Then I should see an error message
-
- @javascript
Scenario: Edit Wiki page that has a path
Given I create a New page with paths
And I click on the "Pages" button
diff --git a/features/steps/admin/broadcast_messages.rb b/features/steps/admin/broadcast_messages.rb
index f6daf852977..6cacdf4764c 100644
--- a/features/steps/admin/broadcast_messages.rb
+++ b/features/steps/admin/broadcast_messages.rb
@@ -1,22 +1,15 @@
class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
- include SharedAdmin
- step 'application already has admin messages' do
- FactoryGirl.create(:broadcast_message, message: "Migration to new server")
+ step 'application already has a broadcast message' do
+ FactoryGirl.create(:broadcast_message, :expired, message: "Migration to new server")
end
- step 'I should be all broadcast messages' do
+ step 'I should see all broadcast messages' do
expect(page).to have_content "Migration to new server"
end
- step 'submit form with new broadcast message' do
- fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST'
- select '2018', from: "broadcast_message_ends_at_1i"
- click_button "Add broadcast message"
- end
-
step 'I should be redirected to admin messages page' do
expect(current_path).to eq admin_broadcast_messages_path
end
@@ -27,10 +20,9 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps
step 'submit form with new customized broadcast message' do
fill_in 'broadcast_message_message', with: 'Application update from 4:00 CST to 5:00 CST'
- click_link "Customize colors"
fill_in 'broadcast_message_color', with: '#f2dede'
fill_in 'broadcast_message_font', with: '#b94a48'
- select '2018', from: "broadcast_message_ends_at_1i"
+ select Date.today.next_year.year, from: "broadcast_message_ends_at_1i"
click_button "Add broadcast message"
end
@@ -38,4 +30,25 @@ class Spinach::Features::AdminBroadcastMessages < Spinach::FeatureSteps
expect(page).to have_content 'Application update from 4:00 CST to 5:00 CST'
expect(page).to have_selector %(div[style="background-color: #f2dede; color: #b94a48"])
end
+
+ step 'I edit an existing broadcast message' do
+ click_link 'Edit'
+ end
+
+ step 'I change the broadcast message text' do
+ fill_in 'broadcast_message_message', with: 'Application update RIGHT NOW'
+ click_button 'Update broadcast message'
+ end
+
+ step 'I should see the updated broadcast message' do
+ expect(page).to have_content "Application update RIGHT NOW"
+ end
+
+ step 'I remove an existing broadcast message' do
+ click_link 'Remove'
+ end
+
+ step 'I should not see the removed broadcast message' do
+ expect(page).not_to have_content 'Migration to new server'
+ end
end
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index 6e57b16ccb6..2363ad797fa 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -28,7 +28,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
end
step 'I should see group milestone with descriptions and expiry date' do
- expect(page).to have_content('expires at Aug 20, 2114')
+ expect(page).to have_content('expires on Aug 20, 2114')
end
step 'I should see group milestone with all issues and MRs assigned to that milestone' do
diff --git a/features/steps/project/builds.rb b/features/steps/project/builds.rb
new file mode 100644
index 00000000000..28395281077
--- /dev/null
+++ b/features/steps/project/builds.rb
@@ -0,0 +1,89 @@
+class Spinach::Features::ProjectBuilds < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedBuilds
+ include RepoHelpers
+
+ step 'I see summary for build' do
+ expect(page).to have_content "Build ##{@build.id}"
+ end
+
+ step 'I see build trace' do
+ expect(page).to have_css '#build-trace'
+ end
+
+ step 'I click artifacts download button' do
+ page.within('.artifacts') { click_link 'Download' }
+ end
+
+ step 'download of build artifacts archive starts' do
+ expect(page.response_headers['Content-Type']).to eq 'application/zip'
+ expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary'
+ end
+
+ step 'I click artifacts browse button' do
+ page.within('.artifacts') { click_link 'Browse' }
+ end
+
+ step 'I should see content of artifacts archive' do
+ page.within('.tree-table') do
+ expect(page).to have_no_content '..'
+ expect(page).to have_content 'other_artifacts_0.1.2'
+ expect(page).to have_content 'ci_artifacts.txt'
+ expect(page).to have_content 'rails_sample.jpg'
+ end
+ end
+
+ step 'I click link to subdirectory within build artifacts' do
+ page.within('.tree-table') { click_link 'other_artifacts_0.1.2' }
+ end
+
+ step 'I should see content of subdirectory within artifacts archive' do
+ page.within('.tree-table') do
+ expect(page).to have_content '..'
+ expect(page).to have_content 'another-subdirectory'
+ expect(page).to have_content 'doc_sample.txt'
+ end
+ end
+
+ step 'recent build artifacts contain directory with UTF-8 characters' do
+ # metadata fixture contains relevant directory
+ end
+
+ step 'I navigate to directory with UTF-8 characters in name' do
+ page.within('.tree-table') { click_link 'tests_encoding' }
+ page.within('.tree-table') { click_link 'utf8 test dir ✓' }
+ end
+
+ step 'I should see content of directory with UTF-8 characters in name' do
+ page.within('.tree-table') do
+ expect(page).to have_content '..'
+ expect(page).to have_content 'regular_file_2'
+ end
+ end
+
+ step 'recent build artifacts contain directory with invalid UTF-8 characters' do
+ # metadata fixture contains relevant directory
+ end
+
+ step 'I navigate to parent directory of directory with invalid name' do
+ page.within('.tree-table') { click_link 'tests_encoding' }
+ end
+
+ step 'I should not see directory with invalid name on the list' do
+ page.within('.tree-table') do
+ expect(page).to have_no_content('non-utf8-dir')
+ end
+ end
+
+ step 'I click download button for a file within build artifacts' do
+ page.within('.tree-table') { first('.artifact-download').click }
+ end
+
+ step 'download of a file extracted from build artifacts should start' do
+ # this will be accelerated by Workhorse
+ response_json = JSON.parse(page.body, symbolize_names: true)
+ expect(response_json[:archive]).to end_with('build_artifacts.zip')
+ expect(response_json[:entry]).to eq Base64.encode64('ci_artifacts.txt')
+ end
+end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index a3141fe3be1..daf6cdaaac8 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -124,4 +124,13 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(page).to have_content "build: pending"
expect(page).to have_content "1 build"
end
+
+ step 'I search "submodules" commits' do
+ fill_in 'commits-search', with: 'submodules'
+ end
+
+ step 'I should see only "submodules" commits' do
+ expect(page).to have_content "More submodules"
+ expect(page).not_to have_content "Change some files"
+ end
end
diff --git a/features/steps/project/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/issues/award_emoji.rb b/features/steps/project/issues/award_emoji.rb
index 1404f34cfe0..2c2ed08655e 100644
--- a/features/steps/project/issues/award_emoji.rb
+++ b/features/steps/project/issues/award_emoji.rb
@@ -24,7 +24,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
page.within '.awards' do
expect do
page.find('.award.active').click
- sleep 0.1
+ sleep 0.3
end.to change{ page.all(".award").size }.from(3).to(2)
end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 4a7ff21d385..8e8c9c57452 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -59,15 +59,14 @@ 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 Assignee'
- 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
diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb
new file mode 100644
index 00000000000..69e8b5cbde5
--- /dev/null
+++ b/features/steps/project/issues/references.rb
@@ -0,0 +1,7 @@
+class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedNote
+ include SharedProject
+ include SharedUser
+end
diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb
new file mode 100644
index 00000000000..ab2ae6847a2
--- /dev/null
+++ b/features/steps/project/merge_requests/references.rb
@@ -0,0 +1,7 @@
+class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedIssuable
+ include SharedNote
+ include SharedProject
+ include SharedUser
+end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
new file mode 100644
index 00000000000..8c1d09d6cc6
--- /dev/null
+++ b/features/steps/project/project_find_file.rb
@@ -0,0 +1,73 @@
+class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+ include SharedProjectTab
+
+ step 'I press "t"' do
+ find('body').native.send_key('t')
+ end
+
+ step 'I click Find File button' do
+ click_link 'Find File'
+ end
+
+ step 'I should see "find file" page' do
+ ensure_active_main_tab('Files')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ step 'I fill in Find by path with "git"' do
+ ensure_active_main_tab('Files')
+ expect(page).to have_selector('.file-finder-holder', count: 1)
+ end
+
+ step 'I fill in file find with "git"' do
+ find_file "git"
+ end
+
+ step 'I fill in file find with "change"' do
+ find_file "change"
+ end
+
+ step 'I fill in file find with "asdfghjklqwertyuizxcvbnm"' do
+ find_file "asdfghjklqwertyuizxcvbnm"
+ end
+
+ step 'I should see "VERSION" in files' do
+ expect(page).to have_content("VERSION")
+ end
+
+ step 'I should not see "VERSION" in files' do
+ expect(page).not_to have_content("VERSION")
+ end
+
+ step 'I should see "CHANGELOG" in files' do
+ expect(page).to have_content("CHANGELOG")
+ end
+
+ step 'I should not see "CHANGELOG" in files' do
+ expect(page).not_to have_content("CHANGELOG")
+ end
+
+ step 'I should see ".gitmodules" in files' do
+ expect(page).to have_content(".gitmodules")
+ end
+
+ step 'I should not see ".gitmodules" in files' do
+ expect(page).not_to have_content(".gitmodules")
+ end
+
+ step 'I should see ".gitignore" in files' do
+ expect(page).to have_content(".gitignore")
+ end
+
+ step 'I should not see ".gitignore" in files' do
+ expect(page).not_to have_content(".gitignore")
+ end
+
+
+ def find_file(text)
+ fill_in 'file_find', with: text
+ end
+end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index 91d227fadbf..d753ae14590 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -132,16 +132,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
expect(current_path).to include 'one/two/three'
end
- step 'I create a New page with an invalid name' do
- click_on 'New Page'
- fill_in 'Page slug', with: 'invalid name'
- click_on 'Create Page'
- end
-
- step 'I should see an error message' do
- expect(page).to have_content "The page slug is invalid"
- end
-
step 'I should see non-escaped link in the pages list' do
expect(page).to have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index eb2ccd9d01e..0bee91d758d 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
- expect(find('div.content ul.center-top-menu li.active')).to have_content(content)
+ expect(find('div.content ul.nav-links li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1)
+ expect(page).to have_selector('div.content ul.nav-links li.active', count: 1)
end
step 'no other sub navs should be active' do
diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb
new file mode 100644
index 00000000000..a83d74e5946
--- /dev/null
+++ b/features/steps/shared/builds.rb
@@ -0,0 +1,28 @@
+module SharedBuilds
+ include Spinach::DSL
+
+ step 'CI is enabled' do
+ @project.enable_ci
+ end
+
+ step 'I have recent build for my project' do
+ ci_commit = create :ci_commit, project: @project, sha: sample_commit.id
+ @build = create :ci_build, commit: ci_commit
+ end
+
+ step 'I visit recent build summary page' do
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ step 'recent build has artifacts available' do
+ artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip'
+ archive = fixture_file_upload(artifacts, 'application/zip')
+ @build.update_attributes(artifacts_file: archive)
+ end
+
+ step 'recent build has artifacts metadata available' do
+ metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ gzip = fixture_file_upload(metadata, 'application/x-gzip')
+ @build.update_attributes(artifacts_metadata: gzip)
+ end
+end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index e6d1b8b8efc..4c5f7488efb 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -5,6 +5,99 @@ module SharedIssuable
find(:css, '.issuable-edit').click
end
+ step 'project "Community" has "Community issue" open issue' do
+ create_issuable_for_project(
+ project_name: 'Community',
+ title: 'Community issue'
+ )
+ end
+
+ step 'project "Community" has "Community fix" open merge request' do
+ create_issuable_for_project(
+ project_name: 'Community',
+ type: :merge_request,
+ title: 'Community fix'
+ )
+ end
+
+ step 'project "Enterprise" has "Enterprise issue" open issue' do
+ create_issuable_for_project(
+ project_name: 'Enterprise',
+ title: 'Enterprise issue'
+ )
+ end
+
+ step 'project "Enterprise" has "Enterprise fix" open merge request' do
+ create_issuable_for_project(
+ project_name: 'Enterprise',
+ type: :merge_request,
+ title: 'Enterprise fix'
+ )
+ end
+
+ step 'I leave a comment referencing issue "Community issue"' do
+ leave_reference_comment(
+ issuable: Issue.find_by(title: 'Community issue'),
+ from_project_name: 'Enterprise'
+ )
+ end
+
+ step 'I leave a comment referencing issue "Community fix"' do
+ leave_reference_comment(
+ issuable: MergeRequest.find_by(title: 'Community fix'),
+ from_project_name: 'Enterprise'
+ )
+ end
+
+ step 'I visit issue page "Enterprise issue"' do
+ issue = Issue.find_by(title: 'Enterprise issue')
+ visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
+ end
+
+ step 'I visit merge request page "Enterprise fix"' do
+ mr = MergeRequest.find_by(title: 'Enterprise fix')
+ visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ end
+
+ step 'I visit issue page "Community issue"' do
+ issue = Issue.find_by(title: 'Community issue')
+ visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
+ end
+
+ step 'I visit issue page "Community fix"' do
+ mr = MergeRequest.find_by(title: 'Community fix')
+ visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
+ end
+
+ step 'I should not see any related merge requests' do
+ page.within '.issue-details' do
+ expect(page).not_to have_content('.merge-requests')
+ end
+ end
+
+ step 'I should see the "Enterprise fix" related merge request' do
+ page.within '.merge-requests' do
+ expect(page).to have_content('1 Related Merge Request')
+ expect(page).to have_content('Enterprise fix')
+ end
+ end
+
+ step 'I should see a note linking to "Enterprise fix" merge request' do
+ visible_note(
+ issuable: MergeRequest.find_by(title: 'Enterprise fix'),
+ from_project_name: 'Community',
+ user_name: 'Mary Jane'
+ )
+ end
+
+ step 'I should see a note linking to "Enterprise issue" issue' do
+ visible_note(
+ issuable: Issue.find_by(title: 'Enterprise issue'),
+ from_project_name: 'Community',
+ user_name: 'Mary Jane'
+ )
+ end
+
step 'I click link "Edit" for the merge request' do
edit_issuable
end
@@ -12,4 +105,45 @@ module SharedIssuable
step 'I click link "Edit" for the issue' do
edit_issuable
end
+
+ def create_issuable_for_project(project_name:, title:, type: :issue)
+ project = Project.find_by(name: project_name)
+
+ attrs = {
+ title: title,
+ author: project.users.first,
+ description: '# Description header'
+ }
+
+ case type
+ when :issue
+ attrs.merge!(project: project)
+ when :merge_request
+ attrs.merge!(
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master'
+ )
+ end
+
+ create(type, attrs)
+ end
+
+ def leave_reference_comment(issuable:, from_project_name:)
+ project = Project.find_by(name: from_project_name)
+
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: "##{issuable.to_reference(project)}"
+ click_button 'Add Comment'
+ end
+ end
+
+ def visible_note(issuable:, from_project_name:, user_name:)
+ project = Project.find_by(name: from_project_name)
+
+ expect(page).to have_content(user_name)
+ expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
+ end
+
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index f6aabfefeff..444d6726f99 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -106,6 +106,10 @@ module SharedNote
end
end
+ step 'I should see no notes at all' do
+ expect(page).to_not have_css('.note')
+ end
+
# Markdown
step 'I leave a comment with a header containing "Comment with a header"' do
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index b33bd332655..4264c9c6f1a 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -259,6 +259,10 @@ module SharedPaths
visit namespace_project_deploy_keys_path(@project.namespace, @project)
end
+ step 'I visit project find file page' do
+ visit namespace_project_find_file_path(@project.namespace, @project, root_ref)
+ end
+
# ----------------------------------------
# "Shop" Project
# ----------------------------------------
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index da643bf3ba9..d3501b5f5cb 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -161,24 +161,33 @@ module SharedProject
end
step '"John Doe" owns private project "Enterprise"' do
- user = user_exists("John Doe", username: "john_doe")
- project = Project.find_by(name: "Enterprise")
- project ||= create(:empty_project, name: "Enterprise", namespace: user.namespace)
- project.team << [user, :master]
+ user_owns_project(
+ user_name: 'John Doe',
+ project_name: 'Enterprise'
+ )
+ end
+
+ step '"Mary Jane" owns private project "Enterprise"' do
+ user_owns_project(
+ user_name: 'Mary Jane',
+ project_name: 'Enterprise'
+ )
end
step '"John Doe" owns internal project "Internal"' do
- user = user_exists("John Doe", username: "john_doe")
- project = Project.find_by(name: "Internal")
- project ||= create :empty_project, :internal, name: 'Internal', namespace: user.namespace
- project.team << [user, :master]
+ user_owns_project(
+ user_name: 'John Doe',
+ project_name: 'Internal',
+ visibility: :internal
+ )
end
step '"John Doe" owns public project "Community"' do
- user = user_exists("John Doe", username: "john_doe")
- project = Project.find_by(name: "Community")
- project ||= create :empty_project, :public, name: 'Community', namespace: user.namespace
- project.team << [user, :master]
+ user_owns_project(
+ user_name: 'John Doe',
+ project_name: 'Community',
+ visibility: :public
+ )
end
step 'public empty project "Empty Public Project"' do
@@ -213,4 +222,12 @@ module SharedProject
expect(page).to have_content("skipped")
end
end
+
+ def user_owns_project(user_name:, project_name:, visibility: :private)
+ user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
+ project = Project.find_by(name: project_name)
+ project ||= create(:empty_project, visibility, name: project_name, namespace: user.namespace)
+ project.team << [user, :master]
+ end
+
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7834262d612..7efe0a0262f 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -54,5 +54,7 @@ module API
mount Keys
mount Tags
mount Triggers
+ mount Builds
+ mount Variables
end
end
diff --git a/lib/api/builds.rb b/lib/api/builds.rb
new file mode 100644
index 00000000000..d293f988165
--- /dev/null
+++ b/lib/api/builds.rb
@@ -0,0 +1,149 @@
+module API
+ # Projects builds API
+ class Builds < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Get a project builds
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
+ # if none provided showing all builds)
+ # Example Request:
+ # GET /projects/:id/builds
+ get ':id/builds' do
+ builds = user_project.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Get builds for a specific commit of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # sha (required) - The SHA id of a commit
+ # scope (optional) - The scope of builds to show (one or array of: pending, running, failed, success, canceled;
+ # if none provided showing all builds)
+ # Example Request:
+ # GET /projects/:id/repository/commits/:sha/builds
+ get ':id/repository/commits/:sha/builds' do
+ commit = user_project.ci_commits.find_by_sha(params[:sha])
+ return not_found! unless commit
+
+ builds = commit.builds.order('id DESC')
+ builds = filter_builds(builds, params[:scope])
+
+ present paginate(builds), with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Get a specific build of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # GET /projects/:id/builds/:build_id
+ get ':id/builds/:build_id' do
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Get a trace of a specific build of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # build_id (required) - The ID of a build
+ # Example Request:
+ # GET /projects/:id/build/:build_id/trace
+ #
+ # TODO: We should use `present_file!` and leave this implementation for backward compatibility (when build trace
+ # is saved in the DB instead of file). But before that, we need to consider how to replace the value of
+ # `runners_token` with some mask (like `xxxxxx`) when sending trace file directly by workhorse.
+ get ':id/builds/:build_id/trace' do
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ header 'Content-Disposition', "infile; filename=\"#{build.id}.log\""
+ content_type 'text/plain'
+ env['api.format'] = :binary
+
+ trace = build.trace
+ body trace
+ end
+
+ # Cancel a specific build of a project
+ #
+ # parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example request:
+ # post /projects/:id/build/:build_id/cancel
+ post ':id/builds/:build_id/cancel' do
+ authorize_manage_builds!
+
+ build = get_build(params[:build_id])
+ return not_found!(build) unless build
+
+ build.cancel
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+
+ # Retry a specific build of a project
+ #
+ # parameters:
+ # id (required) - the id of a project
+ # build_id (required) - the id of a build
+ # example request:
+ # post /projects/:id/build/:build_id/retry
+ post ':id/builds/:build_id/retry' do
+ authorize_manage_builds!
+
+ build = get_build(params[:build_id])
+ return forbidden!('Build is not retryable') unless build && build.retryable?
+
+ build = Ci::Build.retry(build)
+
+ present build, with: Entities::Build,
+ user_can_download_artifacts: can?(current_user, :download_build_artifacts, user_project)
+ end
+ end
+
+ helpers do
+ def get_build(id)
+ user_project.builds.find_by(id: id.to_i)
+ end
+
+ def filter_builds(builds, scope)
+ return builds if scope.nil? || scope.empty?
+
+ available_statuses = ::CommitStatus::AVAILABLE_STATUSES
+ scope =
+ if scope.is_a?(String)
+ [scope]
+ elsif scope.is_a?(Hashie::Mash)
+ scope.values
+ else
+ ['unknown']
+ end
+
+ unknown = scope - available_statuses
+ render_api_error!('Scope contains invalid value(s)', 400) unless unknown.empty?
+
+ builds.where(status: available_statuses && scope)
+ end
+
+ def authorize_manage_builds!
+ authorize! :manage_builds, user_project
+ end
+ end
+ end
+end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index f8511ac5f5c..82a75734de0 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -71,6 +71,7 @@ module API
expose :avatar_url
expose :star_count, :forks_count
expose :open_issues_count, if: lambda { |project, options| project.issues_enabled? && project.default_issues_tracker? }
+ expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
end
class ProjectMember < UserBasic
@@ -166,7 +167,6 @@ module API
class MergeRequest < ProjectEntity
expose :target_branch, :source_branch
- # deprecated, always returns 0
expose :upvotes, :downvotes
expose :author, :assignee, using: Entities::UserBasic
expose :source_project_id, :target_project_id
@@ -366,5 +366,40 @@ module API
class TriggerRequest < Grape::Entity
expose :id, :variables
end
+
+ class Runner < Grape::Entity
+ expose :id
+ expose :description
+ expose :active
+ expose :is_shared
+ expose :name
+ end
+
+ class Build < Grape::Entity
+ expose :id, :status, :stage, :name, :ref, :tag, :coverage
+ expose :created_at, :started_at, :finished_at
+ expose :user, with: User
+ # TODO: download_url in Ci:Build model is an GitLab Web Interface URL, not API URL. We should think on some API
+ # for downloading of artifacts (see: https://gitlab.com/gitlab-org/gitlab-ce/issues/4255)
+ expose :download_url do |repo_obj, options|
+ if options[:user_can_download_artifacts]
+ repo_obj.download_url
+ end
+ end
+ expose :commit, with: RepoCommit do |repo_obj, _options|
+ if repo_obj.respond_to?(:commit)
+ repo_obj.commit.commit_data
+ end
+ end
+ expose :runner, with: Runner
+ end
+
+ class Trigger < Grape::Entity
+ expose :token, :created_at, :updated_at, :deleted_at, :last_used
+ end
+
+ class Variable < Grape::Entity
+ expose :key, :value
+ end
end
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index a4df810e755..6d2380cf47d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -97,11 +97,9 @@ module API
end
def paginate(relation)
- per_page = params[:per_page].to_i
- paginated = relation.page(params[:page]).per(per_page)
- add_pagination_headers(paginated, per_page)
-
- paginated
+ relation.page(params[:page]).per(params[:per_page].to_i).tap do |data|
+ add_pagination_headers(data)
+ end
end
def authenticate!
@@ -289,12 +287,14 @@ module API
# file helpers
- def uploaded_file!(field, uploads_path)
+ def uploaded_file(field, uploads_path)
if params[field]
bad_request!("#{field} is not a file") unless params[field].respond_to?(:filename)
return params[field]
end
+ return nil unless params["#{field}.path"] && params["#{field}.name"]
+
# sanitize file paths
# this requires all paths to exist
required_attributes! %W(#{field}.path)
@@ -327,16 +327,26 @@ module API
private
- def add_pagination_headers(paginated, per_page)
+ def add_pagination_headers(paginated_data)
+ header 'X-Total', paginated_data.total_count.to_s
+ header 'X-Total-Pages', paginated_data.total_pages.to_s
+ header 'X-Per-Page', paginated_data.limit_value.to_s
+ header 'X-Page', paginated_data.current_page.to_s
+ header 'X-Next-Page', paginated_data.next_page.to_s
+ header 'X-Prev-Page', paginated_data.prev_page.to_s
+ header 'Link', pagination_links(paginated_data)
+ end
+
+ def pagination_links(paginated_data)
request_url = request.url.split('?').first
links = []
- links << %(<#{request_url}?page=#{paginated.current_page - 1}&per_page=#{per_page}>; rel="prev") unless paginated.first_page?
- links << %(<#{request_url}?page=#{paginated.current_page + 1}&per_page=#{per_page}>; rel="next") unless paginated.last_page?
- links << %(<#{request_url}?page=1&per_page=#{per_page}>; rel="first")
- links << %(<#{request_url}?page=#{paginated.total_pages}&per_page=#{per_page}>; rel="last")
+ links << %(<#{request_url}?page=#{paginated_data.current_page - 1}&per_page=#{paginated_data.limit_value}>; rel="prev") unless paginated_data.first_page?
+ links << %(<#{request_url}?page=#{paginated_data.current_page + 1}&per_page=#{paginated_data.limit_value}>; rel="next") unless paginated_data.last_page?
+ links << %(<#{request_url}?page=1&per_page=#{paginated_data.limit_value}>; rel="first")
+ links << %(<#{request_url}?page=#{paginated_data.total_pages}&per_page=#{paginated_data.limit_value}>; rel="last")
- header 'Link', links.join(', ')
+ links.join(', ')
end
def abilities
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 3c1c6bda260..5c97fe1c88c 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -211,7 +211,7 @@ module API
unauthorized! unless merge_request.can_be_merged_by?(current_user)
not_allowed! if !merge_request.open? || merge_request.work_in_progress?
- merge_request.check_if_can_be_merged if merge_request.unchecked?
+ merge_request.check_if_can_be_merged
render_api_error!('Branch cannot be merged', 406) unless merge_request.can_be_merged?
diff --git a/lib/api/notes.rb b/lib/api/notes.rb
index 3efdfe2d46e..174473f5371 100644
--- a/lib/api/notes.rb
+++ b/lib/api/notes.rb
@@ -20,7 +20,19 @@ module API
# GET /projects/:id/snippets/:noteable_id/notes
get ":id/#{noteables_str}/:#{noteable_id_str}/notes" do
@noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"])
- present paginate(@noteable.notes), with: Entities::Note
+
+ # We exclude notes that are cross-references and that cannot be viewed
+ # by the current user. By doing this exclusion at this level and not
+ # at the DB query level (which we cannot in that case), the current
+ # page can have less elements than :per_page even if
+ # there's more than one page.
+ notes =
+ # paginate() only works with a relation. This could lead to a
+ # mismatch between the pagination headers info and the actual notes
+ # array returned, but this is really a edge-case.
+ paginate(@noteable.notes).
+ reject { |n| n.cross_reference_not_visible_for?(current_user) }
+ present notes, with: Entities::Note
end
# Get a single +noteable+ note
@@ -35,7 +47,12 @@ module API
get ":id/#{noteables_str}/:#{noteable_id_str}/notes/:note_id" do
@noteable = user_project.send(:"#{noteables_str}").find(params[:"#{noteable_id_str}"])
@note = @noteable.notes.find(params[:note_id])
- present @note, with: Entities::Note
+
+ if @note.cross_reference_not_visible_for?(current_user)
+ not_found!("Note")
+ else
+ present @note, with: Entities::Note
+ end
end
# Create a new +noteable+ note
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a9e0960872a..71bb342f844 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -3,7 +3,7 @@ module API
class Projects < Grape::API
before { authenticate! }
- resource :projects do
+ resource :projects, requirements: { id: /[^\/]+/ } do
helpers do
def map_public_to_visibility_level(attrs)
publik = attrs.delete(:public)
@@ -69,7 +69,8 @@ module API
# Example Request:
# GET /projects/:id
get ":id" do
- present user_project, with: Entities::ProjectWithAccess, user: current_user
+ present user_project, with: Entities::ProjectWithAccess, user: current_user,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
end
# Get events for a single project
@@ -118,7 +119,8 @@ module API
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
- present @project, with: Entities::Project
+ present @project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, @project)
else
if @project.errors[:limit_reached].present?
error!(@project.errors[:limit_reached], 403)
@@ -163,7 +165,8 @@ module API
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
- present @project, with: Entities::Project
+ present @project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, @project)
else
render_validation_error!(@project)
end
@@ -182,8 +185,9 @@ module API
if @forked_project.errors.any?
conflict!(@forked_project.errors.messages)
else
- present @forked_project, with: Entities::Project
- end
+ present @forked_project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, @forked_project)
+ end
end
# Update an existing project
@@ -229,7 +233,8 @@ module API
if user_project.errors.any?
render_validation_error!(user_project)
else
- present user_project, with: Entities::Project
+ present user_project, with: Entities::Project,
+ user_can_admin_project: can?(current_user, :admin_project, user_project)
end
end
@@ -269,7 +274,7 @@ module API
# Remove a forked_from relationship
#
# Parameters:
- # id: (required) - The ID of the project being marked as a fork
+ # id: (required) - The ID of the project being marked as a fork
# Example Request:
# DELETE /projects/:id/fork
delete ":id/fork" do
@@ -278,6 +283,16 @@ module API
user_project.forked_project_link.destroy
end
end
+
+ # Upload a file
+ #
+ # Parameters:
+ # id: (required) - The ID of the project
+ # file: (required) - The file to be uploaded
+ post ":id/uploads" do
+ ::Projects::UploadService.new(user_project, params[:file]).execute
+ end
+
# search for projects current_user has access to
#
# Parameters:
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 47621f443e6..2d8a9e51bb9 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -40,6 +40,27 @@ module API
end
end
+ # Delete tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # Example Request:
+ # DELETE /projects/:id/repository/tags/:tag
+ delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.*/ } do
+ authorize_push_project
+ result = DeleteTagService.new(user_project, current_user).
+ execute(params[:tag_name])
+
+ if result[:status] == :success
+ {
+ tag_name: params[:tag_name]
+ }
+ else
+ render_api_error!(result[:message], result[:return_code])
+ end
+ end
+
# Add release notes to tag
#
# Parameters:
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index 2781f1cf191..5e4964f446c 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -43,6 +43,75 @@ module API
render_api_error!(errors, 400)
end
end
+
+ # Get triggers list
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # page (optional) - The page number for pagination
+ # per_page (optional) - The value of items per page to show
+ # Example Request:
+ # GET /projects/:id/triggers
+ get ':id/triggers' do
+ authenticate!
+ authorize_admin_project
+
+ triggers = user_project.triggers.includes(:trigger_requests)
+ triggers = paginate(triggers)
+
+ present triggers, with: Entities::Trigger
+ end
+
+ # Get specific trigger of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # token (required) - The `token` of a trigger
+ # Example Request:
+ # GET /projects/:id/triggers/:token
+ get ':id/triggers/:token' do
+ authenticate!
+ authorize_admin_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ present trigger, with: Entities::Trigger
+ end
+
+ # Create trigger
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # POST /projects/:id/triggers
+ post ':id/triggers' do
+ authenticate!
+ authorize_admin_project
+
+ trigger = user_project.triggers.create
+
+ present trigger, with: Entities::Trigger
+ end
+
+ # Delete trigger
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # token (required) - The `token` of a trigger
+ # Example Request:
+ # DELETE /projects/:id/triggers/:token
+ delete ':id/triggers/:token' do
+ authenticate!
+ authorize_admin_project
+
+ trigger = user_project.triggers.find_by(token: params[:token].to_s)
+ return not_found!('Trigger') unless trigger
+
+ trigger.destroy
+
+ present trigger, with: Entities::Trigger
+ end
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index 3400f0713ef..fd2128bd179 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -39,7 +39,7 @@ module API
if current_user.is_admin?
present @user, with: Entities::UserFull
else
- present @user, with: Entities::UserBasic
+ present @user, with: Entities::User
end
end
@@ -284,10 +284,12 @@ module API
authenticated_as_admin!
user = User.find_by(id: params[:id])
- if user
+ if !user
+ not_found!('User')
+ elsif !user.ldap_blocked?
user.block
else
- not_found!('User')
+ forbidden!('LDAP blocked users cannot be modified by the API')
end
end
@@ -299,10 +301,12 @@ module API
authenticated_as_admin!
user = User.find_by(id: params[:id])
- if user
- user.activate
- else
+ if !user
not_found!('User')
+ elsif user.ldap_blocked?
+ forbidden!('LDAP blocked users cannot be unblocked by the API')
+ else
+ user.activate
end
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
new file mode 100644
index 00000000000..d9a055f6c92
--- /dev/null
+++ b/lib/api/variables.rb
@@ -0,0 +1,95 @@
+module API
+ # Projects variables API
+ class Variables < Grape::API
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ resource :projects do
+ # Get project variables
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # page (optional) - The page number for pagination
+ # per_page (optional) - The value of items per page to show
+ # Example Request:
+ # GET /projects/:id/variables
+ get ':id/variables' do
+ variables = user_project.variables
+ present paginate(variables), with: Entities::Variable
+ end
+
+ # Get specific variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The `key` of variable
+ # Example Request:
+ # GET /projects/:id/variables/:key
+ get ':id/variables/:key' do
+ key = params[:key]
+ variable = user_project.variables.find_by(key: key.to_s)
+
+ return not_found!('Variable') unless variable
+
+ present variable, with: Entities::Variable
+ end
+
+ # Create a new variable in project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The key of variable
+ # value (required) - The value of variable
+ # Example Request:
+ # POST /projects/:id/variables
+ post ':id/variables' do
+ required_attributes! [:key, :value]
+
+ variable = user_project.variables.create(key: params[:key], value: params[:value])
+
+ if variable.valid?
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Update existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (optional) - The `key` of variable
+ # value (optional) - New value for `value` field of variable
+ # Example Request:
+ # PUT /projects/:id/variables/:key
+ put ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+
+ attrs = attributes_for_keys [:value]
+ if variable.update(attrs)
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Delete existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The ID of a variable
+ # Example Request:
+ # DELETE /projects/:id/variables/:key
+ delete ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+ variable.destroy
+
+ present variable, with: Entities::Variable
+ end
+ end
+ end
+end
diff --git a/lib/banzai/cross_project_reference.rb b/lib/banzai/cross_project_reference.rb
index ba2866e1efa..0257848b6bc 100644
--- a/lib/banzai/cross_project_reference.rb
+++ b/lib/banzai/cross_project_reference.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
# Common methods for ReferenceFilters that support an optional cross-project
# reference.
diff --git a/lib/banzai/filter.rb b/lib/banzai/filter.rb
index fd4fe024252..905c4c0144e 100644
--- a/lib/banzai/filter.rb
+++ b/lib/banzai/filter.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/string/output_safety'
-require 'banzai'
module Banzai
module Filter
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 63ad8910c0f..cdbaecf8d90 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# Issues, Merge Requests, Snippets, Commits and Commit Ranges share
@@ -47,7 +45,17 @@ module Banzai
{ object_sym => LazyReference.new(object_class, node.attr(data_reference)) }
end
- delegate :object_class, :object_sym, :references_in, to: :class
+ def object_class
+ self.class.object_class
+ end
+
+ def object_sym
+ self.class.object_sym
+ end
+
+ def references_in(*args, &block)
+ self.class.references_in(*args, &block)
+ end
def find_object(project, id)
# Implement in child class
@@ -60,27 +68,31 @@ module Banzai
end
def call
- # `#123`
- replace_text_nodes_matching(object_class.reference_pattern) do |content|
- object_link_filter(content, object_class.reference_pattern)
- end
+ if object_class.reference_pattern
+ # `#123`
+ replace_text_nodes_matching(object_class.reference_pattern) do |content|
+ object_link_filter(content, object_class.reference_pattern)
+ end
- # `[Issue](#123)`, which is turned into
- # `<a href="#123">Issue</a>`
- replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
- object_link_filter(link, object_class.reference_pattern, link_text: text)
+ # `[Issue](#123)`, which is turned into
+ # `<a href="#123">Issue</a>`
+ replace_link_nodes_with_href(object_class.reference_pattern) do |link, text|
+ object_link_filter(link, object_class.reference_pattern, link_text: text)
+ end
end
- # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
- # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
- replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
- object_link_filter(text, object_class.link_reference_pattern)
- end
+ if object_class.link_reference_pattern
+ # `http://gitlab.example.com/namespace/project/issues/123`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">http://gitlab.example.com/namespace/project/issues/123</a>`
+ replace_link_nodes_with_text(object_class.link_reference_pattern) do |text|
+ object_link_filter(text, object_class.link_reference_pattern)
+ end
- # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
- # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
- replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
- object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ # `[Issue](http://gitlab.example.com/namespace/project/issues/123)`, which is turned into
+ # `<a href="http://gitlab.example.com/namespace/project/issues/123">Issue</a>`
+ replace_link_nodes_with_href(object_class.link_reference_pattern) do |link, text|
+ object_link_filter(link, object_class.link_reference_pattern, link_text: text)
+ end
end
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index da4ee80c1b5..856f56fb175 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index e67cd45ab9b..470727ee312 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces commit range references with links.
diff --git a/lib/banzai/filter/commit_reference_filter.rb b/lib/banzai/filter/commit_reference_filter.rb
index 9e57608b483..713a56ba949 100644
--- a/lib/banzai/filter/commit_reference_filter.rb
+++ b/lib/banzai/filter/commit_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces commit references with links.
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index 86838e1483c..5952a031626 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -1,5 +1,4 @@
require 'action_controller'
-require 'banzai'
require 'gitlab_emoji'
require 'html/pipeline/filter'
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 6136e73c096..edc26386903 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces external issue tracker references with links.
diff --git a/lib/banzai/filter/external_link_filter.rb b/lib/banzai/filter/external_link_filter.rb
index ac87b9820af..8d368f3b9e7 100644
--- a/lib/banzai/filter/external_link_filter.rb
+++ b/lib/banzai/filter/external_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
new file mode 100644
index 00000000000..fe01dae4850
--- /dev/null
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -0,0 +1,151 @@
+require 'banzai'
+require 'html/pipeline/filter'
+
+module Banzai
+ module Filter
+ # HTML Filter for parsing Gollum's tags in HTML. It's only parses the
+ # following tags:
+ #
+ # - Link to internal pages:
+ #
+ # * [[Bug Reports]]
+ # * [[How to Contribute|Contributing]]
+ #
+ # - Link to external resources:
+ #
+ # * [[http://en.wikipedia.org/wiki/Git_(software)]]
+ # * [[Git|http://en.wikipedia.org/wiki/Git_(software)]]
+ #
+ # - Link internal images, the special attributes will be ignored:
+ #
+ # * [[images/logo.png]]
+ # * [[images/logo.png|alt=Logo]]
+ #
+ # - Link external images, the special attributes will be ignored:
+ #
+ # * [[http://example.com/images/logo.png]]
+ # * [[http://example.com/images/logo.png|alt=Logo]]
+ #
+ # Based on Gollum::Filter::Tags
+ #
+ # Context options:
+ # :project_wiki (required) - Current project wiki.
+ #
+ class GollumTagsFilter < HTML::Pipeline::Filter
+ include ActionView::Helpers::TagHelper
+
+ # Pattern to match tags content that should be parsed in HTML.
+ #
+ # Gollum's tags have been made to resemble the tags of other markups,
+ # especially MediaWiki. The basic syntax is:
+ #
+ # [[tag]]
+ #
+ # Some tags will accept attributes which are separated by pipe
+ # symbols.Some attributes must precede the tag and some must follow it:
+ #
+ # [[prefix-attribute|tag]]
+ # [[tag|suffix-attribute]]
+ #
+ # See https://github.com/gollum/gollum/wiki
+ #
+ # Rubular: http://rubular.com/r/7dQnE5CUCH
+ TAGS_PATTERN = %r{\[\[(.+?)\]\]}
+
+ # Pattern to match allowed image extensions
+ ALLOWED_IMAGE_EXTENSIONS = %r{.+(jpg|png|gif|svg|bmp)\z}i
+
+ def call
+ search_text_nodes(doc).each do |node|
+ content = node.content
+
+ next unless content.match(TAGS_PATTERN)
+
+ html = process_tag($1)
+
+ if html && html != node.content
+ node.replace(html)
+ end
+ end
+
+ doc
+ end
+
+ private
+
+ # Process a single tag into its final HTML form.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML version of the tag.
+ def process_tag(tag)
+ parts = tag.split('|')
+
+ return if parts.size.zero?
+
+ process_image_tag(parts) || process_page_link_tag(parts)
+ end
+
+ # Attempt to process the tag as an image tag.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML if the tag is a valid image tag or nil
+ # if it is not.
+ def process_image_tag(parts)
+ content = parts[0].strip
+
+ return unless image?(content)
+
+ if url?(content)
+ path = content
+ elsif file = project_wiki.find_file(content)
+ path = ::File.join project_wiki_base_path, file.path
+ end
+
+ if path
+ content_tag(:img, nil, src: path)
+ end
+ end
+
+ def image?(path)
+ path =~ ALLOWED_IMAGE_EXTENSIONS
+ end
+
+ def url?(path)
+ path.start_with?(*%w(http https))
+ end
+
+ # Attempt to process the tag as a page link tag.
+ #
+ # tag - The String tag contents (the stuff inside the double brackets).
+ #
+ # Returns the String HTML if the tag is a valid page link tag or nil
+ # if it is not.
+ def process_page_link_tag(parts)
+ if parts.size == 1
+ url = parts[0].strip
+ else
+ name, url = *parts.compact.map(&:strip)
+ end
+
+ content_tag(:a, name || url, href: url)
+ end
+
+ def project_wiki
+ context[:project_wiki]
+ end
+
+ def project_wiki_base_path
+ project_wiki && project_wiki.wiki_base_path
+ end
+
+ # Ensure that a :project_wiki key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project_wiki
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 51180cb901a..9f08aa36e8b 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces issue references with links. References to
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index a3a7a23c1e6..95e7d209119 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces label references with links.
diff --git a/lib/banzai/filter/markdown_filter.rb b/lib/banzai/filter/markdown_filter.rb
index d09cf41df39..0659fed1419 100644
--- a/lib/banzai/filter/markdown_filter.rb
+++ b/lib/banzai/filter/markdown_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index 755b946a34b..57c71708992 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces merge request references with links. References
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
new file mode 100644
index 00000000000..e88b27c1fae
--- /dev/null
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -0,0 +1,22 @@
+require 'banzai'
+
+module Banzai
+ module Filter
+ # HTML filter that replaces milestone references with links.
+ class MilestoneReferenceFilter < AbstractReferenceFilter
+ def self.object_class
+ Milestone
+ end
+
+ def find_object(project, id)
+ project.milestones.find_by(iid: id)
+ end
+
+ def url_for_object(issue, project)
+ h = Gitlab::Application.routes.url_helpers
+ h.namespace_project_milestone_url(project.namespace, project, milestone,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/redactor_filter.rb b/lib/banzai/filter/redactor_filter.rb
index f01a32b5ae5..7141ed7c9bd 100644
--- a/lib/banzai/filter/redactor_filter.rb
+++ b/lib/banzai/filter/redactor_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
@@ -10,7 +9,7 @@ module Banzai
#
class RedactorFilter < HTML::Pipeline::Filter
def call
- doc.css('a.gfm').each do |node|
+ Querying.css(doc, 'a.gfm').each do |node|
unless user_can_see_reference?(node)
# The reference should be replaced by the original text,
# which is not always the same as the rendered text.
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 8ca05ace88c..20bd4f7ee6e 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/string/output_safety'
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
@@ -124,7 +123,7 @@ module Banzai
def replace_link_nodes_with_text(pattern)
return doc if project.nil?
- doc.search('a').each do |node|
+ doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
@@ -133,7 +132,7 @@ module Banzai
next unless link && text
- link = URI.decode(link)
+ link = CGI.unescape(link)
# Ignore ending punctionation like periods or commas
next unless link == text && text =~ /\A#{pattern}/
@@ -162,7 +161,7 @@ module Banzai
def replace_link_nodes_with_href(pattern)
return doc if project.nil?
- doc.search('a').each do |node|
+ doc.xpath('descendant-or-self::a').each do |node|
klass = node.attr('class')
next if klass && klass.include?('gfm')
@@ -170,7 +169,7 @@ module Banzai
text = node.text
next unless link && text
- link = URI.decode(link)
+ link = CGI.unescape(link)
next unless link && link =~ /\A#{pattern}\z/
html = yield link, text
diff --git a/lib/banzai/filter/reference_gatherer_filter.rb b/lib/banzai/filter/reference_gatherer_filter.rb
index 12412ff7ea9..86d484feb90 100644
--- a/lib/banzai/filter/reference_gatherer_filter.rb
+++ b/lib/banzai/filter/reference_gatherer_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
@@ -16,7 +15,7 @@ module Banzai
end
def call
- doc.css('a.gfm').each do |node|
+ Querying.css(doc, 'a.gfm').each do |node|
gather_references(node)
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 5a081125f21..41380627d39 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
@@ -91,7 +90,7 @@ module Banzai
parts = request_path.split('/')
parts.pop if path_type(request_path) != 'tree'
- while parts.length > 1 && path.start_with?('../')
+ while path.start_with?('../')
parts.pop
path.sub!('../', '')
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index d03e3ae4b3c..3f49d492f2f 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'html/pipeline/sanitization_filter'
diff --git a/lib/banzai/filter/snippet_reference_filter.rb b/lib/banzai/filter/snippet_reference_filter.rb
index 1ad5df96f85..c870a42f741 100644
--- a/lib/banzai/filter/snippet_reference_filter.rb
+++ b/lib/banzai/filter/snippet_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces snippet references with links. References to
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index c889cc1e97c..8c5855e5ffc 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'rouge/plugins/redcarpet'
diff --git a/lib/banzai/filter/table_of_contents_filter.rb b/lib/banzai/filter/table_of_contents_filter.rb
index 9b3e67206d5..4056dcd6d64 100644
--- a/lib/banzai/filter/table_of_contents_filter.rb
+++ b/lib/banzai/filter/table_of_contents_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
module Banzai
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index bdf7c2ebdfc..66608c9859c 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'task_list/filter'
module Banzai
@@ -12,13 +11,18 @@ module Banzai
#
# See https://github.com/github/task_list/pull/60
class TaskListFilter < TaskList::Filter
- def add_css_class(node, *new_class_names)
+ def add_css_class_with_fix(node, *new_class_names)
if new_class_names.include?('task-list')
- super if node.children.any? { |c| c['class'] == 'task-list-item' }
- else
- super
+ # Don't add class to all lists
+ return
+ elsif new_class_names.include?('task-list-item')
+ add_css_class_without_fix(node.parent, 'task-list')
end
+
+ add_css_class_without_fix(node, *new_class_names)
end
+
+ alias_method_chain :add_css_class, :fix
end
end
end
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 1a1d0aad8ca..f642aee0967 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline/filter'
require 'uri'
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index 964ab60f614..24f16f8b547 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Filter
# HTML filter that replaces user or group references with links.
diff --git a/lib/banzai/lazy_reference.rb b/lib/banzai/lazy_reference.rb
index 073ec5d9801..1095b4debc7 100644
--- a/lib/banzai/lazy_reference.rb
+++ b/lib/banzai/lazy_reference.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
class LazyReference
def self.load(refs)
diff --git a/lib/banzai/pipeline.rb b/lib/banzai/pipeline.rb
index 4e017809d9d..142a9962eb1 100644
--- a/lib/banzai/pipeline.rb
+++ b/lib/banzai/pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
def self.[](name)
diff --git a/lib/banzai/pipeline/asciidoc_pipeline.rb b/lib/banzai/pipeline/asciidoc_pipeline.rb
index 5e76a817be5..f1331c0ebf9 100644
--- a/lib/banzai/pipeline/asciidoc_pipeline.rb
+++ b/lib/banzai/pipeline/asciidoc_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class AsciidocPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/atom_pipeline.rb b/lib/banzai/pipeline/atom_pipeline.rb
index 957f352aec5..9694e4bc23f 100644
--- a/lib/banzai/pipeline/atom_pipeline.rb
+++ b/lib/banzai/pipeline/atom_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class AtomPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/base_pipeline.rb b/lib/banzai/pipeline/base_pipeline.rb
index cd30009e5c0..db5177db7b3 100644
--- a/lib/banzai/pipeline/base_pipeline.rb
+++ b/lib/banzai/pipeline/base_pipeline.rb
@@ -1,4 +1,3 @@
-require 'banzai'
require 'html/pipeline'
module Banzai
diff --git a/lib/banzai/pipeline/combined_pipeline.rb b/lib/banzai/pipeline/combined_pipeline.rb
index f3bf1809d18..9485199132e 100644
--- a/lib/banzai/pipeline/combined_pipeline.rb
+++ b/lib/banzai/pipeline/combined_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
module CombinedPipeline
diff --git a/lib/banzai/pipeline/description_pipeline.rb b/lib/banzai/pipeline/description_pipeline.rb
index 94c2cb165a5..20e24ace352 100644
--- a/lib/banzai/pipeline/description_pipeline.rb
+++ b/lib/banzai/pipeline/description_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class DescriptionPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/email_pipeline.rb b/lib/banzai/pipeline/email_pipeline.rb
index 14356145a35..e47c384afc1 100644
--- a/lib/banzai/pipeline/email_pipeline.rb
+++ b/lib/banzai/pipeline/email_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class EmailPipeline < FullPipeline
diff --git a/lib/banzai/pipeline/full_pipeline.rb b/lib/banzai/pipeline/full_pipeline.rb
index 72395a5d50e..d47ddfda4be 100644
--- a/lib/banzai/pipeline/full_pipeline.rb
+++ b/lib/banzai/pipeline/full_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class FullPipeline < CombinedPipeline.new(PlainMarkdownPipeline, GfmPipeline)
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 38750b55ec7..b7a38ea8427 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class GfmPipeline < BasePipeline
@@ -22,6 +20,7 @@ module Banzai
Filter::CommitRangeReferenceFilter,
Filter::CommitReferenceFilter,
Filter::LabelReferenceFilter,
+ Filter::MilestoneReferenceFilter,
Filter::TaskListFilter
]
diff --git a/lib/banzai/pipeline/note_pipeline.rb b/lib/banzai/pipeline/note_pipeline.rb
index 89335143852..7890f20f716 100644
--- a/lib/banzai/pipeline/note_pipeline.rb
+++ b/lib/banzai/pipeline/note_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class NotePipeline < FullPipeline
diff --git a/lib/banzai/pipeline/plain_markdown_pipeline.rb b/lib/banzai/pipeline/plain_markdown_pipeline.rb
index 998fd75daa2..3fbc681457b 100644
--- a/lib/banzai/pipeline/plain_markdown_pipeline.rb
+++ b/lib/banzai/pipeline/plain_markdown_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class PlainMarkdownPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 148f24b6ce1..bd338c045f3 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class PostProcessPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/reference_extraction_pipeline.rb b/lib/banzai/pipeline/reference_extraction_pipeline.rb
index 4f9bc9fcccc..eaddccd30a5 100644
--- a/lib/banzai/pipeline/reference_extraction_pipeline.rb
+++ b/lib/banzai/pipeline/reference_extraction_pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
module Pipeline
class ReferenceExtractionPipeline < BasePipeline
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index 6725c9039a9..8b84ab401df 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -1,9 +1,23 @@
-require 'banzai'
-
module Banzai
module Pipeline
class SingleLinePipeline < GfmPipeline
+ def self.filters
+ @filters ||= [
+ Filter::SanitizationFilter,
+
+ Filter::EmojiFilter,
+ Filter::AutolinkFilter,
+ Filter::ExternalLinkFilter,
+ Filter::UserReferenceFilter,
+ Filter::IssueReferenceFilter,
+ Filter::ExternalIssueReferenceFilter,
+ Filter::MergeRequestReferenceFilter,
+ Filter::SnippetReferenceFilter,
+ Filter::CommitRangeReferenceFilter,
+ Filter::CommitReferenceFilter,
+ ]
+ end
end
end
end
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
new file mode 100644
index 00000000000..50b5450e70b
--- /dev/null
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -0,0 +1,11 @@
+require 'banzai'
+
+module Banzai
+ module Pipeline
+ class WikiPipeline < FullPipeline
+ def self.filters
+ super.insert(1, Filter::GollumTagsFilter)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/querying.rb b/lib/banzai/querying.rb
new file mode 100644
index 00000000000..1e1b51e683e
--- /dev/null
+++ b/lib/banzai/querying.rb
@@ -0,0 +1,18 @@
+module Banzai
+ module Querying
+ # Searches a Nokogiri document using a CSS query, optionally optimizing it
+ # whenever possible.
+ #
+ # document - A document/element to search.
+ # query - The CSS query to use.
+ #
+ # Returns a Nokogiri::XML::NodeSet.
+ def self.css(document, query)
+ # When using "a.foo" Nokogiri compiles this to "//a[...]" but
+ # "descendant::a[...]" is quite a bit faster and achieves the same result.
+ xpath = Nokogiri::CSS.xpath_for(query)[0].gsub(%r{^//}, 'descendant::')
+
+ document.xpath(xpath)
+ end
+ end
+end
diff --git a/lib/banzai/reference_extractor.rb b/lib/banzai/reference_extractor.rb
index 2c197d31898..f4079538ec5 100644
--- a/lib/banzai/reference_extractor.rb
+++ b/lib/banzai/reference_extractor.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Banzai
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index 115ae914524..891c0fd7749 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -1,7 +1,5 @@
module Banzai
module Renderer
- CACHE_ENABLED = false
-
# Convert a Markdown String into an HTML-safe String of HTML
#
# Note that while the returned HTML will have been sanitized of dangerous
@@ -20,7 +18,7 @@ module Banzai
cache_key = context.delete(:cache_key)
cache_key = full_cache_key(cache_key, context[:pipeline])
- if cache_key && CACHE_ENABLED
+ if cache_key
Rails.cache.fetch(cache_key) do
cacheless_render(text, context)
end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 15faa6edd84..fb87637b94f 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -78,11 +78,13 @@ module Ci
# Parameters:
# id (required) - The ID of a build
# token (required) - The build authorization token
- # file (required) - The uploaded file
+ # file (required) - Artifacts file
# Parameters (accelerated by GitLab Workhorse):
# file.path - path to locally stored body (generated by Workhorse)
# file.name - real filename as send in Content-Disposition
# file.type - real content type as send in Content-Type
+ # metadata.path - path to locally stored body (generated by Workhorse)
+ # metadata.name - filename (generated by Workhorse)
# Headers:
# BUILD-TOKEN (required) - The build authorization token, the same as token
# Body:
@@ -96,13 +98,20 @@ module Ci
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
authenticate_build_token!(build)
- forbidden!('build is not running') unless build.running?
+ forbidden!('Build is not running!') unless build.running?
- file = uploaded_file!(:file, ArtifactUploader.artifacts_upload_path)
- file_to_large! unless file.size < max_artifacts_size
+ artifacts_upload_path = ArtifactUploader.artifacts_upload_path
+ artifacts = uploaded_file(:file, artifacts_upload_path)
+ metadata = uploaded_file(:metadata, artifacts_upload_path)
- if build.update_attributes(artifacts_file: file)
- present build, with: Entities::Build
+ bad_request!('Missing artifacts file!') unless artifacts
+ file_to_large! unless artifacts.size < max_artifacts_size
+
+ build.artifacts_file = artifacts
+ build.artifacts_metadata = metadata
+
+ if build.save
+ present(build, with: Entities::Build)
else
render_validation_error!(build)
end
@@ -148,6 +157,7 @@ module Ci
not_found! unless build
authenticate_build_token!(build)
build.remove_artifacts_file!
+ build.remove_artifacts_metadata!
end
end
end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 459e3d6bcdb..4c15d58d680 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -150,6 +150,18 @@ module Gitlab
"#{path}.git", tag_name])
end
+ # Gc repository
+ #
+ # path - project path with namespace
+ #
+ # Ex.
+ # gc("gitlab/gitlab-ci")
+ #
+ def gc(path)
+ Gitlab::Utils.system_silent([gitlab_shell_projects_path, 'gc',
+ "#{path}.git"])
+ end
+
# Add new key to gitlab-shell
#
# Ex.
diff --git a/lib/gitlab/build_data_builder.rb b/lib/gitlab/build_data_builder.rb
index 86bfa0a4378..34e949130da 100644
--- a/lib/gitlab/build_data_builder.rb
+++ b/lib/gitlab/build_data_builder.rb
@@ -23,6 +23,7 @@ module Gitlab
build_started_at: build.started_at,
build_finished_at: build.finished_at,
build_duration: build.duration,
+ build_allow_failure: build.allow_failure,
# TODO: do we still need it?
project_id: project.id,
diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb
new file mode 100644
index 00000000000..1344f5d120b
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/metadata.rb
@@ -0,0 +1,109 @@
+require 'zlib'
+require 'json'
+
+module Gitlab
+ module Ci
+ module Build
+ module Artifacts
+ class Metadata
+ class ParserError < StandardError; end
+
+ VERSION_PATTERN = /^[\w\s]+(\d+\.\d+\.\d+)/
+ INVALID_PATH_PATTERN = %r{(^\.?\.?/)|(/\.?\.?/)}
+
+ attr_reader :file, :path, :full_version
+
+ def initialize(file, path)
+ @file, @path = file, path
+ @full_version = read_version
+ end
+
+ def version
+ @full_version.match(VERSION_PATTERN)[1]
+ end
+
+ def errors
+ gzip do |gz|
+ read_string(gz) # version
+ errors = read_string(gz)
+ raise ParserError, 'Errors field not found!' unless errors
+
+ begin
+ JSON.parse(errors)
+ rescue JSON::ParserError
+ raise ParserError, 'Invalid errors field!'
+ end
+ end
+ end
+
+ def find_entries!
+ gzip do |gz|
+ 2.times { read_string(gz) } # version and errors fields
+ match_entries(gz)
+ end
+ end
+
+ def to_entry
+ entries = find_entries!
+ Entry.new(@path, entries)
+ end
+
+ private
+
+ def match_entries(gz)
+ entries = {}
+ match_pattern = %r{^#{Regexp.escape(@path)}[^/]*/?$}
+
+ until gz.eof? do
+ begin
+ path = read_string(gz).force_encoding('UTF-8')
+ meta = read_string(gz).force_encoding('UTF-8')
+
+ next unless path.valid_encoding? && meta.valid_encoding?
+ next unless path =~ match_pattern
+ next if path =~ INVALID_PATH_PATTERN
+
+ entries[path] = JSON.parse(meta, symbolize_names: true)
+ rescue JSON::ParserError, Encoding::CompatibilityError
+ next
+ end
+ end
+
+ entries
+ end
+
+ def read_version
+ gzip do |gz|
+ version_string = read_string(gz)
+
+ unless version_string
+ raise ParserError, 'Artifacts metadata file empty!'
+ end
+
+ unless version_string =~ VERSION_PATTERN
+ raise ParserError, 'Invalid version!'
+ end
+
+ version_string.chomp
+ end
+ end
+
+ def read_uint32(gz)
+ binary = gz.read(4)
+ binary.unpack('L>')[0] if binary
+ end
+
+ def read_string(gz)
+ string_size = read_uint32(gz)
+ return nil unless string_size
+ gz.read(string_size)
+ end
+
+ def gzip(&block)
+ Zlib::GzipReader.open(@file, &block)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/artifacts/metadata/entry.rb b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
new file mode 100644
index 00000000000..25b71fc3275
--- /dev/null
+++ b/lib/gitlab/ci/build/artifacts/metadata/entry.rb
@@ -0,0 +1,119 @@
+module Gitlab
+ module Ci::Build::Artifacts
+ class Metadata
+ ##
+ # Class that represents an entry (path and metadata) to a file or
+ # directory in GitLab CI Build Artifacts binary file / archive
+ #
+ # This is IO-operations safe class, that does similar job to
+ # Ruby's Pathname but without the risk of accessing filesystem.
+ #
+ # This class is working only with UTF-8 encoded paths.
+ #
+ class Entry
+ attr_reader :path, :entries
+ attr_accessor :name
+
+ def initialize(path, entries)
+ @path = path.dup.force_encoding('UTF-8')
+ @entries = entries
+
+ if path.include?("\0")
+ raise ArgumentError, 'Path contains zero byte character!'
+ end
+
+ unless path.valid_encoding?
+ raise ArgumentError, 'Path contains non-UTF-8 byte sequence!'
+ end
+ end
+
+ def directory?
+ blank_node? || @path.end_with?('/')
+ end
+
+ def file?
+ !directory?
+ end
+
+ def has_parent?
+ nodes > 0
+ end
+
+ def parent
+ return nil unless has_parent?
+ self.class.new(@path.chomp(basename), @entries)
+ end
+
+ def basename
+ (directory? && !blank_node?) ? name + '/' : name
+ end
+
+ def name
+ @name || @path.split('/').last.to_s
+ end
+
+ def children
+ return [] unless directory?
+ return @children if @children
+
+ child_pattern = %r{^#{Regexp.escape(@path)}[^/]+/?$}
+ @children = select_entries { |path| path =~ child_pattern }
+ end
+
+ def directories(opts = {})
+ return [] unless directory?
+ dirs = children.select(&:directory?)
+ return dirs unless has_parent? && opts[:parent]
+
+ dotted_parent = parent
+ dotted_parent.name = '..'
+ dirs.prepend(dotted_parent)
+ end
+
+ def files
+ return [] unless directory?
+ children.select(&:file?)
+ end
+
+ def metadata
+ @entries[@path] || {}
+ end
+
+ def nodes
+ @path.count('/') + (file? ? 1 : 0)
+ end
+
+ def blank_node?
+ @path.empty? # "" is considered to be './'
+ end
+
+ def exists?
+ blank_node? || @entries.include?(@path)
+ end
+
+ def empty?
+ children.empty?
+ end
+
+ def to_s
+ @path
+ end
+
+ def ==(other)
+ @path == other.path && @entries == other.entries
+ end
+
+ def inspect
+ "#{self.class.name}: #{@path}"
+ end
+
+ private
+
+ def select_entries
+ selected = @entries.select { |path, _metadata| yield path }
+ selected.map { |path, _metadata| self.class.new(path, @entries) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/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 46a4ef0e31f..7f938780ab1 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -38,7 +38,12 @@ 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')
+
+ rescue ActiveRecord::NoDatabaseError
+ false
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index 177bad4b1cf..2c0a866e8ba 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -53,8 +53,9 @@ module Gitlab
private
def filename?(line)
- line.start_with?('--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
- '--- /tmp/diffy', '+++ /tmp/diffy')
+ line.start_with?( '--- /dev/null', '+++ /dev/null', '--- a', '+++ b',
+ '+++ a', # The line will start with `+++ a` in the reverse diff of an orphan commit
+ '--- /tmp/diffy', '+++ /tmp/diffy')
end
def identification_type(line)
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 2b252b32887..2ca21af5bc8 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -74,7 +74,7 @@ module Gitlab
def sent_notification
return nil unless reply_key
-
+
SentNotification.for(reply_key)
end
@@ -82,10 +82,7 @@ module Gitlab
attachments = Email::AttachmentUploader.new(message).execute(sent_notification.project)
attachments.each do |link|
- text = "[#{link[:alt]}](#{link[:url]})"
- text.prepend("!") if link[:is_image]
-
- reply << "\n\n#{text}"
+ reply << "\n\n#{link[:markdown]}"
end
reply
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 403ebeec474..db580b5e578 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -232,9 +232,7 @@ module Gitlab
return nil if res.nil?
- text = "[#{res['alt']}](#{res['url']})"
- text = "!#{text}" if res['is_image']
- text
+ res[:markdown]
end
def build_attachment_url(rel_url)
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
new file mode 100644
index 00000000000..202263c6742
--- /dev/null
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module GithubImport
+ class BaseFormatter
+ attr_reader :formatter, :project, :raw_data
+
+ def initialize(project, raw_data)
+ @project = project
+ @raw_data = raw_data
+ @formatter = Gitlab::ImportFormatter.new
+ end
+
+ private
+
+ def gl_user_id(github_id)
+ User.joins(:identities).
+ find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
+ try(:id)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
new file mode 100644
index 00000000000..7d58e53991a
--- /dev/null
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module GithubImport
+ class CommentFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ note: note,
+ commit_id: raw_data.commit_id,
+ line_code: line_code,
+ author_id: author_id,
+ created_at: raw_data.created_at,
+ updated_at: raw_data.updated_at
+ }
+ end
+
+ private
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def line_code
+ if on_diff?
+ Gitlab::Diff::LineCode.generate(raw_data.path, raw_data.position, 0)
+ end
+ end
+
+ def on_diff?
+ raw_data.path && raw_data.position
+ end
+
+ def note
+ formatter.author_line(author) + body
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index b5720f6e2cb..18929b9113b 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -1,6 +1,8 @@
module Gitlab
module GithubImport
class Importer
+ include Gitlab::ShellAdapter
+
attr_reader :project, :client
def initialize(project)
@@ -12,39 +14,76 @@ module Gitlab
end
def execute
- #Issues && Comments
+ import_issues && import_pull_requests && import_wiki
+ end
+
+ private
+
+ def import_issues
client.list_issues(project.import_source, state: :all,
sort: :created,
- direction: :asc).each do |issue|
- if issue.pull_request.nil?
-
- body = @formatter.author_line(issue.user.login)
- body += issue.body || ""
+ direction: :asc).each do |raw_data|
+ gh_issue = IssueFormatter.new(project, raw_data)
- if issue.comments > 0
- body += @formatter.comments_header
+ if gh_issue.valid?
+ issue = Issue.create!(gh_issue.attributes)
- client.issue_comments(project.import_source, issue.number).each do |c|
- body += @formatter.comment(c.user.login, c.created_at, c.body)
- end
+ if gh_issue.has_comments?
+ import_comments(gh_issue.number, issue)
end
+ end
+ end
+
+ true
+ rescue ActiveRecord::RecordInvalid
+ false
+ 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
+
+ true
+ rescue ActiveRecord::RecordInvalid
+ false
end
- private
+ def import_comments(issue_number, noteable)
+ comments = client.issue_comments(project.import_source, issue_number)
+ create_comments(comments, noteable)
+ end
+
+ def import_comments_on_diff(pull_request_number, merge_request)
+ comments = client.pull_request_comments(project.import_source, pull_request_number)
+ create_comments(comments, merge_request)
+ end
+
+ def create_comments(comments, noteable)
+ comments.each do |raw_data|
+ comment = CommentFormatter.new(project, raw_data)
+ noteable.notes.create!(comment.attributes)
+ end
+ end
+
+ def import_wiki
+ unless project.wiki_enabled?
+ wiki = WikiFormatter.new(project)
+ gitlab_shell.import_repository(wiki.path_with_namespace, wiki.import_url)
+ project.update_attribute(:wiki_enabled, true)
+ end
- 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
+ true
+ rescue Gitlab::Shell::Error
+ false
end
end
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
new file mode 100644
index 00000000000..1e3ba44f27c
--- /dev/null
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -0,0 +1,66 @@
+module Gitlab
+ module GithubImport
+ class IssueFormatter < BaseFormatter
+ def attributes
+ {
+ project: project,
+ title: raw_data.title,
+ description: description,
+ state: state,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ created_at: raw_data.created_at,
+ updated_at: updated_at
+ }
+ end
+
+ def has_comments?
+ raw_data.comments > 0
+ end
+
+ def number
+ raw_data.number
+ end
+
+ def valid?
+ raw_data.pull_request.nil?
+ end
+
+ private
+
+ def assigned?
+ raw_data.assignee.present?
+ end
+
+ def assignee_id
+ if assigned?
+ gl_user_id(raw_data.assignee.id)
+ end
+ end
+
+ def author
+ raw_data.user.login
+ end
+
+ def author_id
+ gl_user_id(raw_data.user.id) || project.creator_id
+ end
+
+ def body
+ raw_data.body || ""
+ end
+
+ def description
+ @formatter.author_line(author) + body
+ end
+
+ def state
+ raw_data.state == 'closed' ? 'closed' : 'opened'
+ end
+
+ def updated_at
+ state == 'closed' ? raw_data.closed_at : raw_data.updated_at
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 8c27ebd1ce8..474927069a5 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -20,7 +20,8 @@ module Gitlab
visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "github",
import_source: repo.full_name,
- import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
+ import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@"),
+ wiki_enabled: !repo.has_wiki? # If repo has wiki we'll import it later
).execute
project.create_import_data(data: { "github_session" => session_data } )
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
new file mode 100644
index 00000000000..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/github_import/wiki_formatter.rb b/lib/gitlab/github_import/wiki_formatter.rb
new file mode 100644
index 00000000000..6c592ff469c
--- /dev/null
+++ b/lib/gitlab/github_import/wiki_formatter.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module GithubImport
+ class WikiFormatter
+ attr_reader :project
+
+ def initialize(project)
+ @project = project
+ end
+
+ def path_with_namespace
+ "#{project.path_with_namespace}.wiki"
+ end
+
+ def import_url
+ project.import_url.sub(/\.git\z/, ".wiki.git")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index e24b94d6159..59926084d07 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -12,7 +12,7 @@ module Gitlab
end
def execute
- project_identifier = URI.encode(project.import_source, '/')
+ project_identifier = CGI.escape(project.import_source, '/')
#Issues && Comments
issues = client.issues(project_identifier)
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index c438a3d167b..da4435c7308 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -5,7 +5,7 @@
module Gitlab
module LDAP
class Access
- attr_reader :adapter, :provider, :user
+ attr_reader :provider, :user
def self.open(user, &block)
Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
@@ -32,20 +32,20 @@ module Gitlab
end
def allowed?
- if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ if ldap_user
return true unless ldap_config.active_directory
# Block user in GitLab if he/she was blocked in AD
if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- user.block
+ user.ldap_block
false
else
- user.activate if user.blocked? && !ldap_config.block_auto_created_users
+ user.activate if user.ldap_blocked?
true
end
else
# Block the user if they no longer exist in LDAP/AD
- user.block
+ user.ldap_block
false
end
rescue
@@ -59,6 +59,10 @@ module Gitlab
def ldap_config
Gitlab::LDAP::Config.new(provider)
end
+
+ def ldap_user
+ @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ end
end
end
end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 577a890a7d9..df65179bfea 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -70,19 +70,25 @@ module Gitlab
end
def ldap_search(*args)
- results = ldap.search(*args)
+ # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
+ Timeout.timeout(config.timeout) do
+ results = ldap.search(*args)
- if results.nil?
- response = ldap.get_operation_result
+ if results.nil?
+ response = ldap.get_operation_result
- unless response.code.zero?
- Rails.logger.warn("LDAP search error: #{response.message}")
- end
+ unless response.code.zero?
+ Rails.logger.warn("LDAP search error: #{response.message}")
+ end
- []
- else
- results
+ []
+ else
+ results
+ end
end
+ rescue Timeout::Error
+ Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
+ []
end
end
end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 101a3285f4b..aff7ccb157f 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -88,6 +88,10 @@ module Gitlab
options['attributes']
end
+ def timeout
+ options['timeout'].to_i
+ end
+
protected
def base_config
Gitlab.config.ldap
diff --git a/lib/gitlab/markdown/pipeline.rb b/lib/gitlab/markdown/pipeline.rb
index 8f3f43c0e91..699d8b9fc07 100644
--- a/lib/gitlab/markdown/pipeline.rb
+++ b/lib/gitlab/markdown/pipeline.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Gitlab
module Markdown
class Pipeline
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 9470633b065..88a265c6af2 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -1,36 +1,25 @@
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}\/?/
- # Returns the current settings, ensuring we _always_ have a default set of
- # metrics settings (even during tests, when the migrations are lacking,
- # etc). This ensures the application is able to boot up even when the
- # migrations have not been executed.
def self.settings
- if ApplicationSetting.table_exists? and curr = ApplicationSetting.current
- curr
- else
- {
- metrics_pool_size: 16,
- metrics_timeout: 10,
- metrics_enabled: false,
- metrics_method_call_threshold: 10
- }
- end
- end
-
- def self.pool_size
- settings[:metrics_pool_size]
- end
-
- def self.timeout
- settings[:metrics_timeout]
+ @settings ||= {
+ enabled: current_application_settings[:metrics_enabled],
+ pool_size: current_application_settings[:metrics_pool_size],
+ timeout: current_application_settings[:metrics_timeout],
+ method_call_threshold: current_application_settings[:metrics_method_call_threshold],
+ host: current_application_settings[:metrics_host],
+ port: current_application_settings[:metrics_port],
+ sample_interval: current_application_settings[:metrics_sample_interval] || 15
+ }
end
def self.enabled?
- settings[:metrics_enabled]
+ settings[:enabled] || false
end
def self.mri?
@@ -41,43 +30,55 @@ module Gitlab
# This is memoized since this method is called for every instrumented
# method. Loading data from an external cache on every method call slows
# things down too much.
- @method_call_threshold ||= settings[:metrics_method_call_threshold]
+ @method_call_threshold ||= settings[:method_call_threshold]
end
def self.pool
@pool
end
- def self.hostname
- @hostname
- end
+ def self.submit_metrics(metrics)
+ prepared = prepare_metrics(metrics)
- # 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)
+ 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
- if frame
- return frame.path.sub(PATH_REGEX, ''), frame.lineno
- else
- return nil, nil
+ 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
- @hostname = Socket.gethostname
+ 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: pool_size, timeout: timeout) do
- host = settings[:metrics_host]
- db = settings[:metrics_database]
- user = settings[:metrics_username]
- pw = settings[:metrics_password]
+ @pool = ConnectionPool.new(size: settings[:pool_size], timeout: settings[:timeout]) do
+ host = settings[:host]
+ port = settings[:port]
- InfluxDB::Client.new(db, host: host, username: user, password: pw)
+ InfluxDB::Client.
+ new(udp: { host: host, port: port })
end
end
end
diff --git a/lib/gitlab/metrics/instrumentation.rb b/lib/gitlab/metrics/instrumentation.rb
index 06fc2f25948..d9fce2e6758 100644
--- a/lib/gitlab/metrics/instrumentation.rb
+++ b/lib/gitlab/metrics/instrumentation.rb
@@ -123,6 +123,8 @@ module Gitlab
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})
diff --git a/lib/gitlab/metrics/metric.rb b/lib/gitlab/metrics/metric.rb
index f592f4e571f..7ea9555cc8c 100644
--- a/lib/gitlab/metrics/metric.rb
+++ b/lib/gitlab/metrics/metric.rb
@@ -17,16 +17,10 @@ module Gitlab
# Returns a Hash in a format that can be directly written to InfluxDB.
def to_hash
{
- series: @series,
- tags: @tags.merge(
- hostname: Metrics.hostname,
- ruby_engine: RUBY_ENGINE,
- ruby_version: RUBY_VERSION,
- gitlab_version: Gitlab::VERSION,
- process_type: Sidekiq.server? ? 'sidekiq' : 'rails'
- ),
+ series: @series,
+ tags: @tags,
values: @values,
- timestamp: @created_at.to_i
+ timestamp: @created_at.to_i * 1_000_000_000
}
end
end
diff --git a/lib/gitlab/metrics/obfuscated_sql.rb b/lib/gitlab/metrics/obfuscated_sql.rb
deleted file mode 100644
index 481aca56efb..00000000000
--- a/lib/gitlab/metrics/obfuscated_sql.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-module Gitlab
- module Metrics
- # Class for producing SQL queries with sensitive data stripped out.
- class ObfuscatedSQL
- REPLACEMENT = /
- \d+(\.\d+)? # integers, floats
- | '.+?' # single quoted strings
- | \/.+?(?<!\\)\/ # regexps (including escaped slashes)
- /x
-
- MYSQL_REPLACEMENTS = /
- ".+?" # double quoted strings
- /x
-
- # Regex to replace consecutive placeholders with a single one indicating
- # the length. This can be useful when a "IN" statement uses thousands of
- # IDs (storing this would just be a waste of space).
- CONSECUTIVE = /(\?(\s*,\s*)?){2,}/
-
- # sql - The raw SQL query as a String.
- def initialize(sql)
- @sql = sql
- end
-
- # Returns a new, obfuscated SQL query.
- def to_s
- regex = REPLACEMENT
-
- if Gitlab::Database.mysql?
- regex = Regexp.union(regex, MYSQL_REPLACEMENTS)
- end
-
- sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match|
- "#{match.count(',') + 1} values"
- end
-
- # InfluxDB escapes double quotes upon output, so lets get rid of them
- # whenever we can.
- if Gitlab::Database.postgresql?
- sql = sql.delete('"')
- end
-
- sql
- end
- end
- end
-end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index 5c0587c4c51..6f179789d3e 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -32,17 +32,15 @@ module Gitlab
def transaction_from_env(env)
trans = Transaction.new
- trans.add_tag(:request_method, env['REQUEST_METHOD'])
- trans.add_tag(:request_uri, env['REQUEST_URI'])
+ trans.set(:request_uri, env['REQUEST_URI'])
+ trans.set(:request_method, env['REQUEST_METHOD'])
trans
end
def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- label = "#{controller.class.name}##{controller.action_name}"
-
- trans.add_tag(:action, label)
+ controller = env[CONTROLLER_KEY]
+ trans.action = "#{controller.class.name}##{controller.action_name}"
end
end
end
diff --git a/lib/gitlab/metrics/sampler.rb b/lib/gitlab/metrics/sampler.rb
index 828ee1f8c62..fc709222a9b 100644
--- a/lib/gitlab/metrics/sampler.rb
+++ b/lib/gitlab/metrics/sampler.rb
@@ -7,9 +7,14 @@ module Gitlab
# statistics, etc.
class Sampler
# interval - The sampling interval in seconds.
- def initialize(interval = 15)
- @interval = interval
- @metrics = []
+ def initialize(interval = Metrics.settings[:sample_interval])
+ interval_half = interval.to_f / 2
+
+ @interval = interval
+ @interval_steps = (-interval_half..interval_half).step(0.1).to_a
+ @last_step = nil
+
+ @metrics = []
@last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
@last_major_gc = Delta.new(GC.stat[:major_gc_count])
@@ -26,7 +31,7 @@ module Gitlab
Thread.current.abort_on_exception = true
loop do
- sleep(@interval)
+ sleep(sleep_interval)
sample
end
@@ -46,16 +51,15 @@ module Gitlab
end
def flush
- MetricsWorker.perform_async(@metrics.map(&:to_hash))
+ Metrics.submit_metrics(@metrics.map(&:to_hash))
end
def sample_memory_usage
- @metrics << Metric.new('memory_usage', value: System.memory_usage)
+ add_metric('memory_usage', value: System.memory_usage)
end
def sample_file_descriptors
- @metrics << Metric.
- new('file_descriptors', value: System.file_descriptor_count)
+ add_metric('file_descriptors', value: System.file_descriptor_count)
end
if Metrics.mri?
@@ -69,7 +73,7 @@ module Gitlab
counts['Symbol'] = Symbol.all_symbols.length
counts.each do |name, count|
- @metrics << Metric.new('object_counts', { count: count }, type: name)
+ add_metric('object_counts', { count: count }, type: name)
end
end
else
@@ -91,7 +95,34 @@ module Gitlab
stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
- @metrics << Metric.new('gc_statistics', stats)
+ add_metric('gc_statistics', stats)
+ end
+
+ def add_metric(series, values, tags = {})
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
+ end
+
+ # Returns the sleep interval with a random adjustment.
+ #
+ # The random adjustment is put in place to ensure we:
+ #
+ # 1. Don't generate samples at the exact same interval every time (thus
+ # potentially missing anything that happens in between samples).
+ # 2. Don't sample data at the same interval two times in a row.
+ def sleep_interval
+ while step = @interval_steps.sample
+ if step != @last_step
+ @last_step = step
+
+ return @interval + @last_step
+ end
+ end
end
end
end
diff --git a/lib/gitlab/metrics/sidekiq_middleware.rb b/lib/gitlab/metrics/sidekiq_middleware.rb
index ec10707d1fb..fd98aa3412e 100644
--- a/lib/gitlab/metrics/sidekiq_middleware.rb
+++ b/lib/gitlab/metrics/sidekiq_middleware.rb
@@ -5,26 +5,14 @@ module Gitlab
# This middleware is intended to be used as a server-side middleware.
class SidekiqMiddleware
def call(worker, message, queue)
- # We don't want to track the MetricsWorker itself as otherwise we'll end
- # up in an infinite loop.
- if worker.class == MetricsWorker
- yield
- return
- end
-
- trans = Transaction.new
+ trans = Transaction.new("#{worker.class.name}#perform")
begin
trans.run { yield }
ensure
- tag_worker(trans, worker)
trans.finish
end
end
-
- def tag_worker(trans, worker)
- trans.add_tag(:action, "#{worker.class.name}#perform")
- end
end
end
end
diff --git a/lib/gitlab/metrics/subscribers/action_view.rb b/lib/gitlab/metrics/subscribers/action_view.rb
index 7e0dcf99d92..2e9dd4645e3 100644
--- a/lib/gitlab/metrics/subscribers/action_view.rb
+++ b/lib/gitlab/metrics/subscribers/action_view.rb
@@ -19,6 +19,7 @@ module Gitlab
values = values_for(event)
tags = tags_for(event)
+ current_transaction.increment(:view_duration, event.duration)
current_transaction.add_metric(SERIES, values, tags)
end
@@ -32,16 +33,8 @@ module Gitlab
def tags_for(event)
path = relative_path(event.payload[:identifier])
- tags = { view: path }
- file, line = Metrics.last_relative_application_frame
-
- if file and line
- tags[:file] = file
- tags[:line] = line
- end
-
- tags
+ { view: path }
end
def current_transaction
diff --git a/lib/gitlab/metrics/subscribers/active_record.rb b/lib/gitlab/metrics/subscribers/active_record.rb
index d947c128ce2..8008b3bc895 100644
--- a/lib/gitlab/metrics/subscribers/active_record.rb
+++ b/lib/gitlab/metrics/subscribers/active_record.rb
@@ -1,44 +1,18 @@
module Gitlab
module Metrics
module Subscribers
- # Class for tracking raw SQL queries.
- #
- # Queries are obfuscated before being logged to ensure no private data is
- # exposed via InfluxDB/Grafana.
+ # Class for tracking the total query duration of a transaction.
class ActiveRecord < ActiveSupport::Subscriber
attach_to :active_record
- SERIES = 'sql_queries'
-
def sql(event)
return unless current_transaction
- values = values_for(event)
- tags = tags_for(event)
-
- current_transaction.add_metric(SERIES, values, tags)
+ current_transaction.increment(:sql_duration, event.duration)
end
private
- def values_for(event)
- { duration: event.duration }
- end
-
- def tags_for(event)
- sql = ObfuscatedSQL.new(event.payload[:sql]).to_s
- tags = { sql: sql }
-
- file, line = Metrics.last_relative_application_frame
-
- if file and line
- tags[:file] = file
- tags[:line] = line
- end
-
- tags
- end
-
def current_transaction
Transaction.current
end
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 568f9d6ae0c..2578ddc49f4 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -4,45 +4,64 @@ module Gitlab
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
- SERIES = 'transactions'
+ attr_reader :tags, :values
- attr_reader :uuid, :tags
+ attr_accessor :action
def self.current
Thread.current[THREAD_KEY]
end
- # name - The name of this transaction as a String.
- def initialize
+ # action - A String describing the action performed, usually the class
+ # plus method name.
+ def initialize(action = nil)
@metrics = []
- @uuid = SecureRandom.uuid
@started_at = nil
@finished_at = nil
- @tags = {}
+ @values = Hash.new(0)
+ @tags = {}
+ @action = action
+
+ @memory_before = 0
+ @memory_after = 0
end
def duration
@finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
end
+ def allocated_memory
+ @memory_after - @memory_before
+ end
+
def run
Thread.current[THREAD_KEY] = self
- @started_at = Time.now
+ @memory_before = System.memory_usage
+ @started_at = Time.now
yield
ensure
- @finished_at = Time.now
+ @memory_after = System.memory_usage
+ @finished_at = Time.now
Thread.current[THREAD_KEY] = nil
end
def add_metric(series, values, tags = {})
- tags = tags.merge(transaction_id: @uuid)
+ prefix = sidekiq? ? 'sidekiq_' : 'rails_'
+
+ @metrics << Metric.new("#{prefix}#{series}", values, tags)
+ end
+
+ def increment(name, value)
+ @values[name] += value
+ end
- @metrics << Metric.new(series, values, tags)
+ def set(name, value)
+ @values[name] = value
end
def add_tag(key, value)
@@ -55,11 +74,29 @@ module Gitlab
end
def track_self
- add_metric(SERIES, { duration: duration }, @tags)
+ values = { duration: duration, allocated_memory: allocated_memory }
+
+ @values.each do |name, value|
+ values[name] = value
+ end
+
+ add_metric('transactions', values, @tags)
end
def submit
- MetricsWorker.perform_async(@metrics.map(&:to_hash))
+ metrics = @metrics.map do |metric|
+ hash = metric.to_hash
+
+ hash[:tags][:action] ||= @action if @action
+
+ hash
+ end
+
+ Metrics.submit_metrics(metrics)
+ end
+
+ def sidekiq?
+ Sidekiq.server?
end
end
end
diff --git a/lib/gitlab/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 be795649e59..4d830aa45e1 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,5 +1,3 @@
-require 'banzai'
-
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
@@ -19,7 +17,7 @@ module Gitlab
super(text, context.merge(project: project))
end
- %i(user label merge_request snippet commit commit_range).each do |type|
+ %i(user label milestone merge_request snippet commit commit_range).each do |type|
define_method("#{type}s") do
@references[type] ||= references(type, reference_context)
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 0469c5a61c3..2dc2953e328 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -431,7 +431,7 @@ namespace :gitlab do
try_fixing_it(
"sudo chmod -R ug+rwX,o-rwx #{repo_base_path}",
"sudo chmod -R ug-s #{repo_base_path}",
- "find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
+ "sudo find #{repo_base_path} -type d -print0 | sudo xargs -0 chmod g+s"
)
for_more_information(
see_installation_guide_section "GitLab Shell"
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index ebe516ec879..8c63877e51c 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -2,6 +2,8 @@ module Gitlab
class TaskAbortedByUserError < StandardError; end
end
+String.disable_colorization = true unless STDOUT.isatty
+
namespace :gitlab do
# Ask if the user wants to continue
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/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb
index 15824a1c67f..80a418feb3e 100644
--- a/spec/controllers/abuse_reports_controller_spec.rb
+++ b/spec/controllers/abuse_reports_controller_spec.rb
@@ -1,76 +1,46 @@
require 'spec_helper'
describe AbuseReportsController do
- let(:reporter) { create(:user) }
- let(:user) { create(:user) }
- let(:message) { "This user is a spammer" }
+ let(:reporter) { create(:user) }
+ let(:user) { create(:user) }
+ let(:attrs) do
+ attributes_for(:abuse_report) do |hash|
+ hash[:user_id] = user.id
+ end
+ end
before do
sign_in(reporter)
end
- describe "POST create" do
- context "with admin notification email set" do
- let(:admin_email) { "admin@example.com"}
-
- before(:each) do
- stub_application_setting(admin_notification_email: admin_email)
+ describe 'POST create' do
+ context 'with valid attributes' do
+ it 'saves the abuse report' do
+ expect do
+ post :create, abuse_report: attrs
+ end.to change { AbuseReport.count }.by(1)
end
- it "sends a notification email" do
- perform_enqueued_jobs do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
-
- email = ActionMailer::Base.deliveries.last
+ it 'calls notify' do
+ expect_any_instance_of(AbuseReport).to receive(:notify)
- expect(email.to).to eq([admin_email])
- expect(email.subject).to include(user.username)
- expect(email.text_part.body).to include(message)
- end
+ post :create, abuse_report: attrs
end
- it "saves the abuse report" do
- perform_enqueued_jobs do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
- end
- end
- end
+ it 'redirects back to the reported user' do
+ post :create, abuse_report: attrs
- context "without admin notification email set" do
- before(:each) do
- stub_application_setting(admin_notification_email: nil)
+ expect(response).to redirect_to user
end
+ end
- it "does not send a notification email" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.not_to change { ActionMailer::Base.deliveries.count }
- end
+ context 'with invalid attributes' do
+ it 'renders new' do
+ attrs.delete(:user_id)
+ post :create, abuse_report: attrs
- it "saves the abuse report" do
- expect do
- post :create,
- abuse_report: {
- user_id: user.id,
- message: message
- }
- end.to change { AbuseReport.count }.by(1)
+ expect(response).to render_template(:new)
end
end
end
-
end
diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb
new file mode 100644
index 00000000000..c131d22a30a
--- /dev/null
+++ b/spec/controllers/admin/identities_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Admin::IdentitiesController do
+ let(:admin) { create(:admin) }
+ before { sign_in(admin) }
+
+ describe 'UPDATE identity' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
+
+ it 'repairs ldap blocks' do
+ expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
+
+ put :update, user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' }
+ end
+ end
+
+ describe 'DELETE identity' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') }
+
+ it 'repairs ldap blocks' do
+ expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute)
+
+ delete :destroy, user_id: user.username, id: user.ldap_identity.id
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 8b7af4d3a0a..5b1f65d7aff 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -34,17 +34,34 @@ describe Admin::UsersController do
end
describe 'PUT unblock/:id' do
- let(:user) { create(:user) }
-
- before do
- user.block
+ context 'ldap blocked users' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain') }
+
+ before do
+ user.ldap_block
+ end
+
+ it 'will not unblock user' do
+ put :unblock, id: user.username
+ user.reload
+ expect(user.blocked?).to be_truthy
+ expect(flash[:alert]).to eq 'This user cannot be unlocked manually from GitLab'
+ end
end
- it 'unblocks user' do
- put :unblock, id: user.username
- user.reload
- expect(user.blocked?).to be_falsey
- expect(flash[:notice]).to eq 'Successfully unblocked'
+ context 'manually blocked users' do
+ let(:user) { create(:user) }
+
+ before do
+ user.block
+ end
+
+ it 'unblocks user' do
+ put :unblock, id: user.username
+ user.reload
+ expect(user.blocked?).to be_falsey
+ expect(flash[:notice]).to eq 'Successfully unblocked'
+ end
end
end
diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb
new file mode 100644
index 00000000000..038dfeb8466
--- /dev/null
+++ b/spec/controllers/projects/find_file_controller_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Projects::FindFileController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+
+ project.team << [user, :master]
+ controller.instance_variable_set(:@project, project)
+ end
+
+ describe "GET #show" do
+ # Make sure any errors accessing the tree in our views bubble up to this spec
+ render_views
+
+ before do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id)
+ end
+
+ context "valid branch" do
+ let(:id) { 'master' }
+ it { is_expected.to respond_with(:success) }
+ end
+
+ context "invalid branch" do
+ let(:id) { 'invalid-branch' }
+ it { is_expected.to respond_with(:not_found) }
+ end
+ end
+
+ describe "GET #list" do
+ def go(format: 'json')
+ get :list,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: id,
+ format: format
+ end
+
+ context "valid branch" do
+ let(:id) { 'master' }
+ it 'returns an array of file path list' do
+ go
+
+ json = JSON.parse(response.body)
+ is_expected.to respond_with(:success)
+ expect(json).not_to eq(nil)
+ expect(json.length).to be >= 0
+ end
+ end
+
+ context "invalid branch" do
+ let(:id) { 'invalid-branch' }
+
+ it 'responds with status 404' do
+ go
+ is_expected.to respond_with(:not_found)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/sent_notification_controller_spec.rb b/spec/controllers/sent_notification_controller_spec.rb
new file mode 100644
index 00000000000..9ced397bd4a
--- /dev/null
+++ b/spec/controllers/sent_notification_controller_spec.rb
@@ -0,0 +1,26 @@
+require 'rails_helper'
+
+describe SentNotificationsController, type: :controller do
+ let(:user) { create(:user) }
+ let(:issue) { create(:issue, author: user) }
+ let(:sent_notification) { create(:sent_notification, noteable: issue) }
+
+ describe 'GET #unsubscribe' do
+ it 'returns a 404 when calling without existing id' do
+ get(:unsubscribe, id: '0' * 32)
+
+ expect(response.status).to be 404
+ end
+
+ context 'calling with id' do
+ it 'shows a flash message to the user' do
+ get(:unsubscribe, id: sent_notification.reply_key)
+
+ expect(response.status).to be 302
+
+ expect(response).to redirect_to new_user_session_path
+ expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ end
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index d6b4efa9a03..2a81684dfcf 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -212,4 +212,11 @@ FactoryGirl.define do
provider 'ldapmain'
extern_uid 'my-ldap-id'
end
+
+ factory :sent_notification do
+ project
+ recipient factory: :user
+ noteable factory: :issue
+ reply_key "0123456789abcdef" * 2
+ end
end
diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb
index ea0039d39e6..978a7d4cecb 100644
--- a/spec/factories/broadcast_messages.rb
+++ b/spec/factories/broadcast_messages.rb
@@ -6,7 +6,6 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
@@ -18,10 +17,17 @@
FactoryGirl.define do
factory :broadcast_message do
message "MyText"
- starts_at "2013-11-12 13:43:25"
- ends_at "2013-11-12 13:43:25"
- alert_type 1
- color "#555555"
- font "#BBBBBB"
+ starts_at Date.today
+ ends_at Date.tomorrow
+
+ trait :expired do
+ starts_at 5.days.ago
+ ends_at 3.days.ago
+ end
+
+ trait :future do
+ starts_at 5.days.from_now
+ ends_at 6.days.from_now
+ end
end
end
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
index f76e826f138..d2db77f6286 100644
--- a/spec/factories/ci/builds.rb
+++ b/spec/factories/ci/builds.rb
@@ -30,6 +30,7 @@ FactoryGirl.define do
name 'test'
ref 'master'
tag false
+ created_at 'Di 29. Okt 09:50:00 CET 2013'
started_at 'Di 29. Okt 09:51:28 CET 2013'
finished_at 'Di 29. Okt 09:53:28 CET 2013'
commands 'ls -a'
@@ -42,6 +43,10 @@ FactoryGirl.define do
commit factory: :ci_commit
+ trait :canceled do
+ status 'canceled'
+ end
+
after(:build) do |build, evaluator|
build.project = build.commit.project
end
@@ -54,5 +59,11 @@ FactoryGirl.define do
factory :ci_build_tag do
tag true
end
+
+ factory :ci_build_with_trace do
+ after(:create) do |build, evaluator|
+ build.trace = 'BUILD TRACE'
+ end
+ end
end
end
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
index db053c610cd..2c0d004d267 100644
--- a/spec/factories/ci/trigger_requests.rb
+++ b/spec/factories/ci/trigger_requests.rb
@@ -3,6 +3,8 @@
FactoryGirl.define do
factory :ci_trigger_request, class: Ci::TriggerRequest do
factory :ci_trigger_request_with_variables do
+ trigger factory: :ci_trigger
+
variables do
{
TRIGGER_KEY: 'TRIGGER_VALUE'
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
new file mode 100644
index 00000000000..8f62d64411b
--- /dev/null
+++ b/spec/factories/ci/variables.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: ci_variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+# gl_project_id :integer
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_variable, class: Ci::Variable do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value 'VARIABLE_VALUE'
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 5b4d7f41bc4..0c6a881f868 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -2,25 +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
-# merge_error :string(255)
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
#
FactoryGirl.define do
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 112213377ff..c14b99606ba 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -29,6 +29,13 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
#
FactoryGirl.define do
diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb
index 72764b1629d..b955d0b0c46 100644
--- a/spec/features/admin/admin_builds_spec.rb
+++ b/spec/features/admin/admin_builds_spec.rb
@@ -1,69 +1,98 @@
require 'spec_helper'
-describe "Admin Builds" do
- let(:commit) { FactoryGirl.create :ci_commit }
- let(:build) { FactoryGirl.create :ci_build, commit: commit }
-
+describe 'Admin Builds' do
before do
login_as :admin
end
- describe "GET /admin/builds" do
- before do
- build
- visit admin_builds_path
- end
-
- it { expect(page).to have_content "Running" }
- it { expect(page).to have_content build.short_sha }
- end
+ describe 'GET /admin/builds' do
+ let(:commit) { create(:ci_commit) }
- describe "Tabs" do
- it "shows all builds" do
- FactoryGirl.create :ci_build, commit: commit, status: "pending"
- FactoryGirl.create :ci_build, commit: commit, status: "running"
- FactoryGirl.create :ci_build, commit: commit, status: "success"
- FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ context 'All tab' do
+ context 'when have builds' do
+ it 'shows all builds' do
+ create(:ci_build, commit: commit, status: :pending)
+ create(:ci_build, commit: commit, status: :running)
+ create(:ci_build, commit: commit, status: :success)
+ create(:ci_build, commit: commit, status: :failed)
- visit admin_builds_path
+ visit admin_builds_path
- within ".center-top-menu" do
- click_on "All"
+ expect(page).to have_selector('.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
- expect(page.all(".build-link").size).to eq(4)
+ context 'when have no builds' do
+ it 'shows a message' do
+ visit admin_builds_path
+
+ expect(page).to have_selector('.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
- it "shows finished builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ context 'Running tab' do
+ context 'when have running builds' do
+ it 'shows running builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :success)
+ build3 = create(:ci_build, commit: commit, status: :failed)
+
+ visit admin_builds_path(scope: :running)
+
+ expect(page).to have_selector('.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
- visit admin_builds_path
+ context 'when have no builds running' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :success)
- within ".center-top-menu" do
- click_on "Finished"
- end
+ visit admin_builds_path(scope: :running)
- expect(page.find(".build-link")).not_to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build1.id)
- expect(page.find(".build-link")).to have_content(build2.id)
+ expect(page).to have_selector('.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
- it "shows running builds" do
- build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
- build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
- build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+ context 'Finished tab' do
+ context 'when have finished builds' do
+ it 'shows finished builds' do
+ build1 = create(:ci_build, commit: commit, status: :pending)
+ build2 = create(:ci_build, commit: commit, status: :running)
+ build3 = create(:ci_build, commit: commit, status: :success)
+
+ visit admin_builds_path(scope: :finished)
+
+ expect(page).to have_selector('.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
- visit admin_builds_path
+ context 'when have no builds finished' do
+ it 'shows a message' do
+ create(:ci_build, commit: commit, status: :running)
- within ".center-top-menu" do
- click_on "Running"
- end
+ visit admin_builds_path(scope: :finished)
- expect(page.find(".build-link")).to have_content(build.id)
- expect(page.find(".build-link")).not_to have_content(build2.id)
- expect(page.find(".build-link")).not_to have_content(build3.id)
+ expect(page).to have_selector('.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/builds_spec.rb b/spec/features/builds_spec.rb
index f0031a0a247..d37bd103714 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -15,11 +15,11 @@ describe "Builds" do
context "Running scope" do
before do
@build.run!
- visit namespace_project_builds_path(@project.namespace, @project)
+ visit namespace_project_builds_path(@project.namespace, @project, scope: :running)
end
- it { expect(page).to have_content 'Running' }
- it { expect(page).to have_content 'Cancel running' }
+ it { expect(page).to have_selector('.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 }
@@ -31,21 +31,22 @@ describe "Builds" do
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 running' }
+ it { expect(page).to have_link 'Cancel running' }
end
context "All builds" do
before do
@project.builds.running_or_pending.each(&:success)
- visit namespace_project_builds_path(@project.namespace, @project, scope: :all)
+ visit namespace_project_builds_path(@project.namespace, @project)
end
- it { expect(page).to have_content 'All' }
+ it { expect(page).to have_selector('.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 running' }
+ it { expect(page).to_not have_link 'Cancel running' }
end
end
@@ -56,8 +57,12 @@ describe "Builds" do
click_link "Cancel running"
end
- it { expect(page).to have_content 'No builds to show' }
- it { expect(page).to_not have_content 'Cancel running' }
+ it { expect(page).to have_selector('.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
@@ -75,7 +80,11 @@ describe "Builds" do
visit namespace_project_build_path(@project.namespace, @project, @build)
end
- it { expect(page).to have_content 'Download artifacts' }
+ it 'has button to download artifacts' do
+ page.within('.artifacts') do
+ expect(page).to have_content 'Download'
+ end
+ end
end
end
@@ -106,7 +115,7 @@ describe "Builds" do
before do
@build.update_attributes(artifacts_file: artifacts_file)
visit namespace_project_build_path(@project.namespace, @project, @build)
- click_link 'Download artifacts'
+ page.within('.artifacts') { click_link 'Download' }
end
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb
index e6e73e5e67c..30e29d9d552 100644
--- a/spec/features/ci_lint_spec.rb
+++ b/spec/features/ci_lint_spec.rb
@@ -35,5 +35,13 @@ describe 'CI Lint' do
expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml')
end
end
+
+ describe 'YAML revalidate' do
+ let(:yaml_content) { 'my yaml content' }
+
+ it 'loads previous YAML content after validation' do
+ expect(page).to have_field('content', with: 'my yaml content', type: 'textarea')
+ end
+ end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index a2fb3e4c75d..e844e681ebf 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -127,15 +127,15 @@ describe 'Issues', feature: true do
it 'sorts by newest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created)
- expect(first_issue).to include('foo')
- expect(last_issue).to include('baz')
+ expect(first_issue).to include('baz')
+ expect(last_issue).to include('foo')
end
it 'sorts by oldest' do
visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created)
- expect(first_issue).to include('baz')
- expect(last_issue).to include('foo')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('baz')
end
it 'sorts by most recently updated' do
@@ -190,8 +190,8 @@ describe 'Issues', feature: true do
sort: sort_value_oldest_created,
assignee_id: user2.id)
- expect(first_issue).to include('bar')
- expect(last_issue).to include('foo')
+ expect(first_issue).to include('foo')
+ expect(last_issue).to include('bar')
expect(page).not_to have_content 'baz'
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index fdd8cf07b12..12fd8d37210 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do
end
end
- context 'default pipeline' do
- before(:all) do
- @feat = MarkdownFeature.new
+ before(:all) do
+ @feat = MarkdownFeature.new
- # `markdown` helper expects a `@project` variable
- @project = @feat.project
+ # `markdown` helper expects a `@project` variable
+ @project = @feat.project
+ end
+ context 'default pipeline' do
+ before(:all) do
@html = markdown(@feat.raw_markdown)
end
@@ -212,6 +214,7 @@ describe 'GitLab Markdown', feature: true do
expect(doc).to reference_commit_ranges
expect(doc).to reference_commits
expect(doc).to reference_labels
+ expect(doc).to reference_milestones
end
end
@@ -220,6 +223,57 @@ describe 'GitLab Markdown', feature: true do
end
end
+ context 'wiki pipeline' do
+ before do
+ @project_wiki = @feat.project_wiki
+
+ file = Gollum::File.new(@project_wiki.wiki)
+ expect(file).to receive(:path).and_return('images/example.jpg')
+ expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file)
+
+ @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki })
+ end
+
+ it_behaves_like 'all pipelines'
+
+ it 'includes RelativeLinkFilter' do
+ expect(doc).not_to parse_relative_links
+ end
+
+ it 'includes EmojiFilter' do
+ expect(doc).to parse_emoji
+ end
+
+ it 'includes TableOfContentsFilter' do
+ expect(doc).to create_header_links
+ end
+
+ it 'includes AutolinkFilter' do
+ expect(doc).to create_autolinks
+ end
+
+ it 'includes all reference filters' do
+ aggregate_failures do
+ expect(doc).to reference_users
+ expect(doc).to reference_issues
+ expect(doc).to reference_merge_requests
+ expect(doc).to reference_snippets
+ expect(doc).to reference_commit_ranges
+ expect(doc).to reference_commits
+ expect(doc).to reference_labels
+ expect(doc).to reference_milestones
+ end
+ end
+
+ it 'includes TaskListFilter' do
+ expect(doc).to parse_task_lists
+ end
+
+ it 'includes GollumTagsFilter' do
+ expect(doc).to parse_gollum_tags
+ end
+ end
+
# Fake a `current_user` helper
def current_user
@feat.user
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 74b148f5d17..9a01c89ae2a 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -80,8 +80,10 @@ feature 'Project', feature: true do
visit namespace_project_path(project.namespace, project)
end
- it { expect(page).to have_content('You have Master access to this project.') }
- it { expect(page).to have_link('Leave this project') }
+ it 'click project-settings and find leave project' do
+ find('#project-settings-button').click
+ expect(page).to have_link('Leave Project')
+ end
end
def remove_with_confirm(button_text, confirm_with)
diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb
deleted file mode 100644
index 4f6a000822e..00000000000
--- a/spec/finders/groups_finder_spec.rb
+++ /dev/null
@@ -1,48 +0,0 @@
-require 'spec_helper'
-
-describe GroupsFinder do
- describe '#execute' do
- let(:user) { create(:user) }
-
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:group3) { create(:group) }
- let(:group4) { create(:group, public: true) }
-
- let!(:public_project) { create(:project, :public, group: group1) }
- let!(:internal_project) { create(:project, :internal, group: group2) }
- let!(:private_project) { create(:project, :private, group: group3) }
-
- let(:finder) { described_class.new }
-
- describe 'with a user' do
- subject { finder.execute(user) }
-
- describe 'when the user is not a member of any groups' do
- it { is_expected.to eq([group4, group2, group1]) }
- end
-
- describe 'when the user is a member of a group' do
- before do
- group3.add_user(user, Gitlab::Access::DEVELOPER)
- end
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
-
- describe 'when the user is a member of a private project' do
- before do
- private_project.team.add_user(user, Gitlab::Access::DEVELOPER)
- end
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
- end
-
- describe 'without a user' do
- subject { finder.execute }
-
- it { is_expected.to eq([group4, group1]) }
- end
- end
-end
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
deleted file mode 100644
index 2d9068cc720..00000000000
--- a/spec/finders/joined_groups_finder_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'spec_helper'
-
-describe JoinedGroupsFinder do
- describe '#execute' do
- let(:source_user) { create(:user) }
- let(:current_user) { create(:user) }
-
- let(:group1) { create(:group) }
- let(:group2) { create(:group) }
- let(:group3) { create(:group) }
- let(:group4) { create(:group, public: true) }
-
- let!(:public_project) { create(:project, :public, group: group1) }
- let!(:internal_project) { create(:project, :internal, group: group2) }
- let!(:private_project) { create(:project, :private, group: group3) }
-
- let(:finder) { described_class.new(source_user) }
-
- before do
- [group1, group2, group3, group4].each do |group|
- group.add_user(source_user, Gitlab::Access::MASTER)
- end
- end
-
- describe 'with a current user' do
- describe 'when the current user has access to the projects of the source user' do
- before do
- private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER)
- end
-
- subject { finder.execute(current_user) }
-
- it { is_expected.to eq([group4, group3, group2, group1]) }
- end
-
- describe 'when the current user does not have access to the projects of the source user' do
- subject { finder.execute(current_user) }
-
- it { is_expected.to eq([group4, group2, group1]) }
- end
- end
-
- describe 'without a current user' do
- subject { finder.execute }
-
- it { is_expected.to eq([group4, group1]) }
- end
- end
-end
diff --git a/spec/fixtures/ci_build_artifacts.zip b/spec/fixtures/ci_build_artifacts.zip
new file mode 100644
index 00000000000..dae976d918e
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts.zip
Binary files differ
diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz
new file mode 100644
index 00000000000..fe9d4c8c661
--- /dev/null
+++ b/spec/fixtures/ci_build_artifacts_metadata.gz
Binary files differ
diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
index e8dfc5c0eb1..fe6d42acee2 100644
--- a/spec/fixtures/markdown.md.erb
+++ b/spec/fixtures/markdown.md.erb
@@ -214,6 +214,13 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link)
- Link to label by reference: [Label](<%= label.to_reference %>)
+#### MilestoneReferenceFilter
+
+- Milestone: <%= milestone.to_reference %>
+- Milestone in another project: <%= xmilestone.to_reference(project) %>
+- Ignored in code: `<%= milestone.to_reference %>`
+- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>)
+
### Task Lists
- [ ] Incomplete task 1
@@ -223,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e
- [ ] Incomplete sub-task 2
- [x] Complete sub-task 1
- [X] Complete task 2
+
+#### Gollum Tags
+
+- [[linked-resource]]
+- [[link-text|linked-resource]]
+- [[http://example.com]]
+- [[link-text|http://example.com/pdfs/gollum.pdf]]
+- [[images/example.jpg]]
+- [[http://example.com/images/example.jpg]]
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 68527c3a4f8..30e353148a8 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -240,7 +240,7 @@ describe ApplicationHelper do
describe 'time_ago_with_tooltip' do
def element(*arguments)
Time.zone = 'UTC'
- time = Time.zone.parse('2015-07-02 08:00')
+ time = Time.zone.parse('2015-07-02 08:23')
element = helper.time_ago_with_tooltip(time, *arguments)
Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
@@ -251,15 +251,15 @@ describe ApplicationHelper do
end
it 'includes the date string' do
- expect(element.text).to eq '2015-07-02 08:00:00 UTC'
+ expect(element.text).to eq '2015-07-02 08:23:00 UTC'
end
it 'has a datetime attribute' do
- expect(element.attr('datetime')).to eq '2015-07-02T08:00:00Z'
+ expect(element.attr('datetime')).to eq '2015-07-02T08:23:00Z'
end
it 'has a formatted title attribute' do
- expect(element.attr('title')).to eq 'Jul 02, 2015 8:00am'
+ expect(element.attr('title')).to eq 'Jul 2, 2015 8:23am'
end
it 'includes a default js-timeago class' do
@@ -285,6 +285,10 @@ describe ApplicationHelper do
it 'allows the script tag to be excluded' do
expect(element(skip_js: true)).not_to include 'script'
end
+
+ it 'converts to Time' do
+ expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error
+ end
end
describe 'render_markup' do
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index c7c6f45d144..157cc4665a2 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -1,22 +1,60 @@
require 'spec_helper'
describe BroadcastMessagesHelper do
- describe 'broadcast_styling' do
- let(:broadcast_message) { double(color: '', font: '') }
+ describe 'broadcast_message' do
+ it 'returns nil when no current message' do
+ expect(helper.broadcast_message(nil)).to be_nil
+ end
+
+ it 'includes the current message' do
+ current = double(message: 'Current Message')
+
+ allow(helper).to receive(:broadcast_message_style).and_return(nil)
+
+ expect(helper.broadcast_message(current)).to include 'Current Message'
+ end
+
+ it 'includes custom style' do
+ current = double(message: 'Current Message')
+
+ allow(helper).to receive(:broadcast_message_style).and_return('foo')
+
+ expect(helper.broadcast_message(current)).to include 'style="foo"'
+ end
+ end
+
+ describe 'broadcast_message_style' do
+ it 'defaults to no style' do
+ broadcast_message = spy
+
+ expect(helper.broadcast_message_style(broadcast_message)).to eq ''
+ end
+
+ it 'allows custom style' do
+ broadcast_message = double(color: '#f2dede', font: '#b94a48')
+
+ expect(helper.broadcast_message_style(broadcast_message)).
+ to match('background-color: #f2dede; color: #b94a48')
+ end
+ end
+
+ describe 'broadcast_message_status' do
+ it 'returns Active' do
+ message = build(:broadcast_message)
+
+ expect(helper.broadcast_message_status(message)).to eq 'Active'
+ end
+
+ it 'returns Expired' do
+ message = build(:broadcast_message, :expired)
- context "default style" do
- it "should have no style" do
- expect(broadcast_styling(broadcast_message)).to eq ''
- end
+ expect(helper.broadcast_message_status(message)).to eq 'Expired'
end
- context "customized style" do
- let(:broadcast_message) { double(color: "#f2dede", font: '#b94a48') }
+ it 'returns Pending' do
+ message = build(:broadcast_message, :future)
- it "should have a customized style" do
- expect(broadcast_styling(broadcast_message)).
- to match('background-color: #f2dede; color: #b94a48')
- end
+ expect(helper.broadcast_message_status(message)).to eq 'Pending'
end
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 762ec25c4f5..9a05b21335c 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
+ helper.instance_variable_set(:@project_wiki, @wiki)
end
- it "should use GitLab Flavored Markdown for markdown files" do
+ it "should use Wiki pipeline for markdown files" do
allow(@wiki).to receive(:format).and_return(:markdown)
- expect(helper).to receive(:markdown).with('wiki content')
+ expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki)
helper.render_wiki_content(@wiki)
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index fd7107779f6..cf632f594c7 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -2,10 +2,8 @@ require 'rails_helper'
describe PageLayoutHelper do
describe 'page_description' do
- it 'defaults to value returned by page_description_default helper' do
- allow(helper).to receive(:page_description_default).and_return('Foo')
-
- expect(helper.page_description).to eq 'Foo'
+ it 'defaults to nil' do
+ expect(helper.page_description).to eq nil
end
it 'returns the last-pushed description' do
@@ -42,58 +40,32 @@ describe PageLayoutHelper do
end
end
- describe 'page_description_default' do
- it 'uses Project description when available' do
- project = double(description: 'Project Description')
- helper.instance_variable_set(:@project, project)
-
- expect(helper.page_description_default).to eq 'Project Description'
- end
-
- it 'uses brand_title when Project description is nil' do
- project = double(description: nil)
- helper.instance_variable_set(:@project, project)
-
- expect(helper).to receive(:brand_title).and_return('Brand Title')
- expect(helper.page_description_default).to eq 'Brand Title'
- end
-
- it 'falls back to brand_title' do
- allow(helper).to receive(:brand_title).and_return('Brand Title')
-
- expect(helper.page_description_default).to eq 'Brand Title'
- end
- end
-
describe 'page_image' do
it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
end
- context 'with @project' do
- it 'uses Project avatar if available' do
- project = double(avatar_url: 'http://example.com/uploads/avatar.png')
- helper.instance_variable_set(:@project, project)
+ %w(project user group).each do |type|
+ context "with @#{type} assigned" do
+ it "uses #{type.titlecase} avatar if available" do
+ object = double(avatar_url: 'http://example.com/uploads/avatar.png')
+ assign(type, object)
- expect(helper.page_image).to eq project.avatar_url
- end
+ expect(helper.page_image).to eq object.avatar_url
+ end
- it 'falls back to the default' do
- project = double(avatar_url: nil)
- helper.instance_variable_set(:@project, project)
+ it 'falls back to the default when avatar_url is nil' do
+ object = double(avatar_url: nil)
+ assign(type, object)
- expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
end
- end
-
- context 'with @user' do
- it 'delegates to avatar_icon helper' do
- user = double('User')
- helper.instance_variable_set(:@user, user)
-
- expect(helper).to receive(:avatar_icon).with(user)
- helper.page_image
+ context "with no assignments" do
+ it 'falls back to the default' do
+ expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
+ end
end
end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index ebe9c29d91c..f0d553f5f1d 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -43,7 +43,7 @@ describe SearchHelper do
end
it "includes the public group" do
- group = create(:group, public: true)
+ group = create(:group)
expect(search_autocomplete_opts(group.name).size).to eq(1)
end
diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml
index e867e4de2b9..1701652c61e 100644
--- a/spec/javascripts/fixtures/zen_mode.html.haml
+++ b/spec/javascripts/fixtures/zen_mode.html.haml
@@ -1,9 +1,8 @@
.zennable
- %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' }
.zen-backdrop
- %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'}
- %a.zen-enter-link{tabindex: '-1'}
+ %textarea#note_note.js-gfm-input.markdown-area
+ %a.js-zen-enter(tabindex="-1" href="#")
%i.fa.fa-expand
- Edit in fullscreen
- %a.zen-leave-link
+ Edit in fullscreen
+ %a.js-zen-leave(tabindex="-1" href="#")
%i.fa.fa-compress
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index 7e67c778861..86ba9dd8e96 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -26,10 +26,10 @@ describe 'reopen/close issue', ->
fixture.load('issues_show.html')
@issue = new Issue()
it 'closes an issue', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://gitlab.com/issues/6/close')
- obj.success saved: true
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://gitlab.com/issues/6/close')
+ req.success saved: true
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
@@ -44,12 +44,12 @@ describe 'reopen/close issue', ->
expect($('div.status-box-closed')).toBeVisible()
expect($('div.status-box-open')).toBeHidden()
- it 'fails to closes an issue with success:false', ->
+ it 'fails to close an issue with success:false', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
- obj.success saved: false
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami')
+ req.success saved: false
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
@@ -69,10 +69,10 @@ describe 'reopen/close issue', ->
it 'fails to closes an issue with HTTP error', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://goesnowhere.nothing/whereami')
- obj.error()
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://goesnowhere.nothing/whereami')
+ req.error()
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
@@ -91,10 +91,10 @@ describe 'reopen/close issue', ->
expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.')
it 'reopens an issue', ->
- $.ajax = (obj) ->
- expect(obj.type).toBe('PUT')
- expect(obj.url).toBe('http://gitlab.com/issues/6/reopen')
- obj.success saved: true
+ spyOn(jQuery, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PUT')
+ expect(req.url).toBe('http://gitlab.com/issues/6/reopen')
+ req.success saved: true
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee
index 4cb3836755f..b790fce01ed 100644
--- a/spec/javascripts/zen_mode_spec.js.coffee
+++ b/spec/javascripts/zen_mode_spec.js.coffee
@@ -15,14 +15,6 @@ describe 'ZenMode', ->
# Set this manually because we can't actually scroll the window
@zen.scroll_position = 456
- # Ohmmmmmmm
- enterZen = ->
- $('.zen-toggle-comment').prop('checked', true).trigger('change')
-
- # Wh- what was that?!
- exitZen = ->
- $('.zen-toggle-comment').prop('checked', false).trigger('change')
-
describe 'on enter', ->
it 'pauses Mousetrap', ->
spyOn(Mousetrap, 'pause')
@@ -35,16 +27,14 @@ describe 'ZenMode', ->
expect('textarea').not.toHaveAttr('style')
describe 'in use', ->
- beforeEach ->
- enterZen()
+ beforeEach -> enterZen()
it 'exits on Escape', ->
- $(document).trigger(jQuery.Event('keydown', {keyCode: 27}))
- expect($('.zen-toggle-comment').prop('checked')).toBe(false)
+ escapeKeydown()
+ expect($('.zen-backdrop')).not.toHaveClass('fullscreen')
describe 'on exit', ->
- beforeEach ->
- enterZen()
+ beforeEach -> enterZen()
it 'unpauses Mousetrap', ->
spyOn(Mousetrap, 'unpause')
@@ -52,6 +42,10 @@ describe 'ZenMode', ->
expect(Mousetrap.unpause).toHaveBeenCalled()
it 'restores the scroll position', ->
- spyOn(@zen, 'restoreScroll')
+ spyOn(@zen, 'scrollTo')
exitZen()
- expect(@zen.restoreScroll).toHaveBeenCalledWith(456)
+ expect(@zen.scrollTo).toHaveBeenCalled()
+
+enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm
+exitZen = -> $('a.js-zen-leave').click()
+escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27}))
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
new file mode 100644
index 00000000000..38baa819957
--- /dev/null
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe Banzai::Filter::GollumTagsFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:user) { double }
+ let(:project_wiki) { ProjectWiki.new(project, user) }
+
+ describe 'validation' do
+ it 'ensure that a :project_wiki key exists in context' do
+ expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :project_wiki"
+ end
+ end
+
+ context 'linking internal images' do
+ it 'creates img tag if image exists' do
+ file = Gollum::File.new(project_wiki.wiki)
+ expect(file).to receive(:path).and_return('images/image.jpg')
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file)
+
+ tag = '[[images/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
+ end
+
+ it 'does not creates img tag if image does not exist' do
+ expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(nil)
+
+ tag = '[[images/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.css('img').size).to eq 0
+ end
+ end
+
+ context 'linking external images' do
+ it 'creates img tag for valid URL' do
+ tag = '[[http://example.com/image.jpg]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg"
+ end
+
+ it 'does not creates img tag for invalid URL' do
+ tag = '[[http://example.com/image.pdf]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.css('img').size).to eq 0
+ end
+ end
+
+ context 'linking external resources' do
+ it "the created link's text will be equal to the resource's text" do
+ tag = '[[http://example.com]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'http://example.com'
+ expect(doc.at_css('a')['href']).to eq 'http://example.com'
+ end
+
+ it "the created link's text will be link-text" do
+ tag = '[[link-text|http://example.com/pdfs/gollum.pdf]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'link-text'
+ expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf'
+ end
+ end
+
+ context 'linking internal resources' do
+ it "the created link's text will be equal to the resource's text" do
+ tag = '[[wiki-slug]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'wiki-slug'
+ expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ end
+
+ it "the created link's text will be link-text" do
+ tag = '[[link-text|wiki-slug]]'
+ doc = filter("See #{tag}", project_wiki: project_wiki)
+
+ expect(doc.at_css('a').text).to eq 'link-text'
+ expect(doc.at_css('a')['href']).to eq 'wiki-slug'
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
new file mode 100644
index 00000000000..ebf3d7489b5
--- /dev/null
+++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MilestoneReferenceFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:project) { create(:project, :public) }
+ let(:milestone) { create(:milestone, project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>milestone #{milestone.to_reference}</#{elem}>"
+ expect(reference_filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline.
+ # Milestone reference behavior in the full Markdown pipeline is tested elsewhere.
+ let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') }
+
+ it 'links to a valid reference' do
+ doc = reference_filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_milestone_url(project.namespace, project, milestone)
+ end
+
+ it 'links with adjacent text' do
+ doc = reference_filter("milestone (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/)
+ end
+
+ it 'includes a title attribute' do
+ doc = reference_filter("milestone #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}"
+ end
+
+ it 'escapes the title attribute' do
+ milestone.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = reference_filter("milestone #{reference}")
+ expect(doc.text).to eq "milestone #{milestone.title}"
+ end
+
+ it 'includes default classes' do
+ doc = reference_filter("milestone #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone'
+ end
+
+ it 'includes a data-project attribute' do
+ doc = reference_filter("milestone #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-project')
+ expect(link.attr('data-project')).to eq project.id.to_s
+ end
+
+ it 'includes a data-milestone attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-milestone')
+ expect(link.attr('data-milestone')).to eq milestone.id.to_s
+ end
+
+ it 'adds to the results hash' do
+ result = reference_pipeline_result("milestone #{reference}")
+ expect(result[:references][:milestone]).to eq [milestone]
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 0b3e5ecbc9f..0e6685f0ffb 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -92,6 +92,14 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
end
+ it 'rebuilds relative URL for a file in the repository root' do
+ relative_link = link('../README.md')
+ doc = filter(relative_link, requested_path: 'doc/some-file.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/README.md"
+ end
+
it 'rebuilds relative URL for a file in the repo with an anchor' do
doc = filter(link('README.md#section'))
expect(doc.at_css('a')['href']).
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
index f2e3a44478d..569cbc885c7 100644
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ b/spec/lib/banzai/filter/task_list_filter_spec.rb
@@ -7,4 +7,10 @@ describe Banzai::Filter::TaskListFilter, lib: true do
exp = act = %(<ul><li>Item</li></ul>)
expect(filter(act).to_html).to eq exp
end
+
+ it 'applies `task-list` to single-item task lists' do
+ act = filter('<ul><li>[ ] Task 1</li></ul>')
+
+ expect(act.to_html).to start_with '<ul class="task-list">'
+ end
end
diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb
new file mode 100644
index 00000000000..27da2a7439c
--- /dev/null
+++ b/spec/lib/banzai/querying_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe Banzai::Querying do
+ describe '.css' do
+ it 'optimizes queries for elements with classes' do
+ document = double(:document)
+
+ expect(document).to receive(:xpath).with(/^descendant::a/)
+
+ described_class.css(document, 'a.gfm')
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index c90133fbf03..d15100fc6d8 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
module Ci
describe GitlabCiYamlProcessor, lib: true do
let(:path) { 'path' }
-
+
describe "#builds_for_ref" do
let(:type) { 'test' }
@@ -29,7 +29,7 @@ module Ci
when: "on_success"
})
end
-
+
describe :only do
it "does not return builds if only has another branch" do
config = YAML.dump({
@@ -517,7 +517,7 @@ module Ci
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, path)
diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb
index 839b30f1ff4..38be9448794 100644
--- a/spec/lib/gitlab/build_data_builder_spec.rb
+++ b/spec/lib/gitlab/build_data_builder_spec.rb
@@ -14,6 +14,7 @@ describe 'Gitlab::BuildDataBuilder' do
it { expect(data[:tag]).to eq(build.tag) }
it { expect(data[:build_id]).to eq(build.id) }
it { expect(data[:build_status]).to eq(build.status) }
+ it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:project_id]).to eq(build.project.id) }
it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
new file mode 100644
index 00000000000..41257103ead
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb
@@ -0,0 +1,168 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do
+ let(:entries) do
+ { 'path/' => {},
+ 'path/dir_1/' => {},
+ 'path/dir_1/file_1' => {},
+ 'path/dir_1/file_b' => {},
+ 'path/dir_1/subdir/' => {},
+ 'path/dir_1/subdir/subfile' => {},
+ 'path/second_dir' => {},
+ 'path/second_dir/dir_3/file_2' => {},
+ 'path/second_dir/dir_3/file_3'=> {},
+ 'another_directory/'=> {},
+ 'another_file' => {},
+ '/file/with/absolute_path' => {} }
+ end
+
+ def path(example)
+ entry(example.metadata[:path])
+ end
+
+ def entry(path)
+ described_class.new(path, entries)
+ end
+
+ describe '/file/with/absolute_path', path: '/file/with/absolute_path' do
+ subject { |example| path(example) }
+
+ it { is_expected.to be_file }
+ it { is_expected.to have_parent }
+
+ describe '#basename' do
+ subject { |example| path(example).basename }
+ it { is_expected.to eq 'absolute_path' }
+ end
+ end
+
+ describe 'path/dir_1/', path: 'path/dir_1/' do
+ subject { |example| path(example) }
+ it { is_expected.to have_parent }
+ it { is_expected.to be_directory }
+
+ describe '#basename' do
+ subject { |example| path(example).basename }
+ it { is_expected.to eq 'dir_1/' }
+ end
+
+ describe '#name' do
+ subject { |example| path(example).name }
+ it { is_expected.to eq 'dir_1' }
+ end
+
+ describe '#parent' do
+ subject { |example| path(example).parent }
+ it { is_expected.to eq entry('path/') }
+ end
+
+ describe '#children' do
+ subject { |example| path(example).children }
+
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b'),
+ entry('path/dir_1/subdir/')
+ end
+ end
+
+ describe '#files' do
+ subject { |example| path(example).files }
+
+ it { is_expected.to all(be_file) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/file_1'),
+ entry('path/dir_1/file_b')
+ end
+ end
+
+ describe '#directories' do
+ context 'without options' do
+ subject { |example| path(example).directories }
+
+ it { is_expected.to all(be_directory) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it { is_expected.to contain_exactly entry('path/dir_1/subdir/') }
+ end
+
+ context 'with option parent: true' do
+ subject { |example| path(example).directories(parent: true) }
+
+ it { is_expected.to all(be_directory) }
+ it { is_expected.to all(be_an_instance_of described_class) }
+ it do
+ is_expected.to contain_exactly entry('path/dir_1/subdir/'),
+ entry('path/')
+ end
+ end
+
+ describe '#nodes' do
+ subject { |example| path(example).nodes }
+ it { is_expected.to eq 2 }
+ end
+
+ describe '#exists?' do
+ subject { |example| path(example).exists? }
+ it { is_expected.to be true }
+ end
+
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be false }
+ end
+ end
+ end
+
+ describe 'empty path', path: '' do
+ subject { |example| path(example) }
+ it { is_expected.to_not have_parent }
+
+ describe '#children' do
+ subject { |example| path(example).children }
+ it { expect(subject.count).to eq 3 }
+ end
+
+ end
+
+ describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do
+ describe '#nodes' do
+ subject { |example| path(example).nodes }
+ it { is_expected.to eq 4 }
+ end
+ end
+
+ describe 'non-existent/', path: 'non-existent/' do
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be true }
+ end
+
+ describe '#exists?' do
+ subject { |example| path(example).exists? }
+ it { is_expected.to be false }
+ end
+ end
+
+ describe 'another_directory/', path: 'another_directory/' do
+ describe '#empty?' do
+ subject { |example| path(example).empty? }
+ it { is_expected.to be true }
+ end
+ end
+
+ describe '#metadata' do
+ let(:entries) do
+ { 'path/' => { name: '/path/' },
+ 'path/file1' => { name: '/path/file1' },
+ 'path/file2' => { name: '/path/file2' } }
+ end
+
+ subject do
+ described_class.new('path/file1', entries).metadata[:name]
+ end
+
+ it { is_expected.to eq '/path/file1' }
+ end
+end
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
new file mode 100644
index 00000000000..828eedfa7b0
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb
@@ -0,0 +1,84 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Artifacts::Metadata do
+ def metadata(path = '')
+ described_class.new(metadata_file_path, path)
+ end
+
+ let(:metadata_file_path) do
+ Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz'
+ end
+
+ context 'metadata file exists' do
+ describe '#find_entries! empty string' do
+ subject { metadata('').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).to contain_exactly 'ci_artifacts.txt',
+ 'other_artifacts_0.1.2/',
+ 'rails_sample.jpg',
+ 'tests_encoding/'
+ end
+
+ it 'matches metadata for every path' do
+ expect(subject.keys.count).to eq 4
+ end
+
+ it 'return Hashes for each metadata' do
+ expect(subject.values).to all(be_kind_of(Hash))
+ end
+ end
+
+ describe '#find_entries! other_artifacts_0.1.2/' do
+ subject { metadata('other_artifacts_0.1.2/').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/',
+ 'other_artifacts_0.1.2/doc_sample.txt',
+ 'other_artifacts_0.1.2/another-subdirectory/'
+ end
+ end
+
+ describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do
+ subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! }
+
+ it 'matches correct paths' do
+ expect(subject.keys).
+ to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/',
+ 'other_artifacts_0.1.2/another-subdirectory/empty_directory/',
+ 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif'
+ end
+ end
+
+ describe '#to_entry' do
+ subject { metadata('').to_entry }
+ it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) }
+ end
+
+ describe '#full_version' do
+ subject { metadata('').full_version }
+ it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' }
+ end
+
+ describe '#version' do
+ subject { metadata('').version }
+ it { is_expected.to eq '0.0.1' }
+ end
+
+ describe '#errors' do
+ subject { metadata('').errors }
+ it { is_expected.to eq({}) }
+ end
+ end
+
+ context 'metadata file does not exist' do
+ let(:metadata_file_path) { '' }
+
+ describe '#find_entries!' do
+ it 'raises error' do
+ expect { metadata.find_entries! }.to raise_error(Errno::ENOENT)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index b535413bbd4..abe179cd4af 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -42,7 +42,7 @@ describe Gitlab::Email::Receiver, lib: true do
context "when the email was auto generated" do
let!(:reply_key) { '636ca428858779856c226bb145ef4fad' }
let!(:email_raw) { fixture_file("emails/auto_reply.eml") }
-
+
it "raises an AutoGeneratedEmailError" do
expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError)
end
@@ -90,7 +90,7 @@ describe Gitlab::Email::Receiver, lib: true do
context "when the reply is blank" do
let!(:email_raw) { fixture_file("emails/no_content_reply.eml") }
-
+
it "raises an EmptyEmailError" do
expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError)
end
@@ -107,13 +107,16 @@ describe Gitlab::Email::Receiver, lib: true do
end
context "when everything is fine" do
+ let(:markdown) { "![image](uploads/image.png)" }
+
before do
allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return(
[
{
url: "uploads/image.png",
is_image: true,
- alt: "image"
+ alt: "image",
+ markdown: markdown
}
]
)
@@ -132,7 +135,7 @@ describe Gitlab::Email::Receiver, lib: true do
note = noteable.notes.last
- expect(note.note).to include("![image](uploads/image.png)")
+ expect(note.note).to include(markdown)
end
end
end
diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
new file mode 100644
index 00000000000..a324a82e69f
--- /dev/null
+++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::CommentFormatter, lib: true do
+ let(:project) { create(:project) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') }
+ let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') }
+ let(:base_data) do
+ {
+ body: "I'm having a problem with this.",
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+ end
+
+ subject(:comment) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when do not reference a portion of the diff' do
+ let(:raw_data) { OpenStruct.new(base_data) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nI'm having a problem with this.",
+ commit_id: nil,
+ line_code: nil,
+ author_id: project.creator_id,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when on a portion of the diff' do
+ let(:diff_data) do
+ {
+ body: 'Great stuff',
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...',
+ path: 'file1.txt',
+ position: 1
+ }
+ end
+
+ let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ note: "*Created by: octocat*\n\nGreat stuff",
+ commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e',
+ line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1',
+ author_id: project.creator_id,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(comment.attributes).to eq(expected)
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(comment.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(comment.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
new file mode 100644
index 00000000000..fd05428b322
--- /dev/null
+++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb
@@ -0,0 +1,139 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::IssueFormatter, lib: true do
+ let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) }
+ let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+
+ let(:base_data) do
+ {
+ number: 1347,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil
+ }
+ end
+
+ subject(:issue) { described_class.new(project, raw_data)}
+
+ describe '#attributes' do
+ context 'when issue is open' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'opened',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: updated_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when issue is closed' do
+ let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') }
+ let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) }
+
+ it 'returns formatted attributes' do
+ expected = {
+ project: project,
+ title: 'Found a bug',
+ description: "*Created by: octocat*\n\nI'm having a problem with this.",
+ state: 'closed',
+ author_id: project.creator_id,
+ assignee_id: nil,
+ created_at: created_at,
+ updated_at: closed_at
+ }
+
+ expect(issue.attributes).to eq(expected)
+ end
+ end
+
+ context 'when it is assigned to someone' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) }
+
+ it 'returns nil as assignee_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:assignee_id)).to be_nil
+ end
+
+ it 'returns GitLab user id as assignee_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id
+ end
+ end
+
+ context 'when author is a GitLab user' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) }
+
+ it 'returns project#creator_id as author_id when is not a GitLab user' do
+ expect(issue.attributes.fetch(:author_id)).to eq project.creator_id
+ end
+
+ it 'returns GitLab user id as author_id when is a GitLab user' do
+ gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github')
+
+ expect(issue.attributes.fetch(:author_id)).to eq gl_user.id
+ end
+ end
+ end
+
+ describe '#has_comments?' do
+ context 'when number of comments is greater than zero' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) }
+
+ it 'returns true' do
+ expect(issue.has_comments?).to eq true
+ end
+ end
+
+ context 'when number of comments is equal to zero' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) }
+
+ it 'returns false' do
+ expect(issue.has_comments?).to eq false
+ end
+ end
+ end
+
+ describe '#number' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) }
+
+ it 'returns pull request number' do
+ expect(issue.number).to eq 1347
+ end
+ end
+
+ describe '#valid?' do
+ context 'when mention a pull request' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) }
+
+ it 'returns false' do
+ expect(issue.valid?).to eq false
+ end
+ end
+
+ context 'when does not mention a pull request' do
+ let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) }
+
+ it 'returns true' do
+ expect(issue.valid?).to eq true
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
new file mode 100644
index 00000000000..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/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
new file mode 100644
index 00000000000..aed2aa39e3a
--- /dev/null
+++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::WikiFormatter, lib: true do
+ let(:project) do
+ create(:project, namespace: create(:namespace, path: 'gitlabhq'),
+ import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git')
+ end
+
+ subject(:wiki) { described_class.new(project)}
+
+ describe '#path_with_namespace' do
+ it 'appends .wiki to project path' do
+ expect(wiki.path_with_namespace).to eq 'gitlabhq/gitlabhq.wiki'
+ end
+ end
+
+ describe '#import_url' do
+ it 'returns URL of the wiki repository' do
+ expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index a628d0c0157..32a19bf344b 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -13,64 +13,58 @@ describe Gitlab::LDAP::Access, lib: true do
end
it { is_expected.to be_falsey }
-
+
it 'should block user in GitLab' do
access.allowed?
expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
end
end
context 'when the user is found' do
before do
- allow(Gitlab::LDAP::Person).
- to receive(:find_by_dn).and_return(:ldap_user)
+ allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
end
context 'and the user is disabled via active directory' do
before do
- allow(Gitlab::LDAP::Person).
- to receive(:disabled_via_active_directory?).and_return(true)
+ allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
end
it { is_expected.to be_falsey }
- it "should block user in GitLab" do
+ it 'should block user in GitLab' do
access.allowed?
expect(user).to be_blocked
+ expect(user).to be_ldap_blocked
end
end
context 'and has no disabled flag in active diretory' do
before do
- user.block
-
- allow(Gitlab::LDAP::Person).
- to receive(:disabled_via_active_directory?).and_return(false)
+ allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
end
it { is_expected.to be_truthy }
context 'when auto-created users are blocked' do
-
before do
- allow_any_instance_of(Gitlab::LDAP::Config).
- to receive(:block_auto_created_users).and_return(true)
+ user.block
end
- it "does not unblock user in GitLab" do
+ it 'does not unblock user in GitLab' do
access.allowed?
expect(user).to be_blocked
+ expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic
end
end
- context "when auto-created users are not blocked" do
-
+ context 'when auto-created users are not blocked' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).
- to receive(:block_auto_created_users).and_return(false)
+ user.ldap_block
end
- it "should unblock user in GitLab" do
+ it 'should unblock user in GitLab' do
access.allowed?
expect(user).not_to be_blocked
end
@@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do
context 'without ActiveDirectory enabled' do
before do
allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
- allow_any_instance_of(Gitlab::LDAP::Config).
- to receive(:active_directory).and_return(false)
+ allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false)
end
it { is_expected.to be_truthy }
diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb
index a7eab9d11cc..2a37cd40dde 100644
--- a/spec/lib/gitlab/metrics/instrumentation_spec.rb
+++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb
@@ -48,6 +48,9 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
+ expect(transaction).to receive(:increment).
+ with(:method_duration, a_kind_of(Numeric))
+
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy.foo')
@@ -102,6 +105,9 @@ describe Gitlab::Metrics::Instrumentation do
allow(described_class).to receive(:transaction).
and_return(transaction)
+ expect(transaction).to receive(:increment).
+ with(:method_duration, a_kind_of(Numeric))
+
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy#bar')
diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb
index ec39bc9cce8..f718d536130 100644
--- a/spec/lib/gitlab/metrics/metric_spec.rb
+++ b/spec/lib/gitlab/metrics/metric_spec.rb
@@ -37,12 +37,6 @@ describe Gitlab::Metrics::Metric do
it 'includes the tags' do
expect(hash[:tags]).to be_an_instance_of(Hash)
-
- expect(hash[:tags][:hostname]).to be_an_instance_of(String)
- expect(hash[:tags][:ruby_engine]).to be_an_instance_of(String)
- expect(hash[:tags][:ruby_version]).to be_an_instance_of(String)
- expect(hash[:tags][:gitlab_version]).to be_an_instance_of(String)
- expect(hash[:tags][:process_type]).to be_an_instance_of(String)
end
it 'includes the values' do
diff --git a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb b/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb
deleted file mode 100644
index 0f01ee588c9..00000000000
--- a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Metrics::ObfuscatedSQL do
- describe '#to_s' do
- describe 'using single values' do
- it 'replaces a single integer' do
- sql = described_class.new('SELECT x FROM y WHERE a = 10')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- it 'replaces a single float' do
- sql = described_class.new('SELECT x FROM y WHERE a = 10.5')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- it 'replaces a single quoted string' do
- sql = described_class.new("SELECT x FROM y WHERE a = 'foo'")
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- if Gitlab::Database.mysql?
- it 'replaces a double quoted string' do
- sql = described_class.new('SELECT x FROM y WHERE a = "foo"')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
- end
-
- it 'replaces a single regular expression' do
- sql = described_class.new('SELECT x FROM y WHERE a = /foo/')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
-
- it 'replaces regular expressions using escaped slashes' do
- sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
- end
- end
-
- describe 'using consecutive values' do
- it 'replaces multiple integers' do
- sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
- end
-
- it 'replaces multiple floats' do
- sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
- end
-
- it 'replaces multiple single quoted strings' do
- sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')")
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
- end
-
- if Gitlab::Database.mysql?
- it 'replaces multiple double quoted strings' do
- sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
- end
- end
-
- it 'replaces multiple regular expressions' do
- sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)')
-
- expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
- end
- end
-
- if Gitlab::Database.postgresql?
- it 'replaces double quotes' do
- sql = described_class.new('SELECT "x" FROM "y"')
-
- expect(sql.to_s).to eq('SELECT x FROM y')
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index a143fe4cfcd..b99be4e1060 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -40,9 +40,9 @@ describe Gitlab::Metrics::RackMiddleware do
expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
end
- it 'tags the transaction with the request method and URI' do
- expect(transaction.tags[:request_method]).to eq('GET')
- expect(transaction.tags[:request_uri]).to eq('/foo')
+ it 'stores the request method and URI in the transaction as values' do
+ expect(transaction.values[:request_method]).to eq('GET')
+ expect(transaction.values[:request_uri]).to eq('/foo')
end
end
@@ -57,7 +57,7 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.tag_controller(transaction, env)
- expect(transaction.tags[:action]).to eq('TestController#show')
+ expect(transaction.action).to eq('TestController#show')
end
end
end
diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb
index 69376c0b79b..38da77adc9f 100644
--- a/spec/lib/gitlab/metrics/sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/sampler_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::Metrics::Sampler do
describe '#start' do
it 'gathers a sample at a given interval' do
- expect(sampler).to receive(:sleep).with(5)
+ expect(sampler).to receive(:sleep).with(a_kind_of(Numeric))
expect(sampler).to receive(:sample)
expect(sampler).to receive(:loop).and_yield
@@ -38,7 +38,7 @@ describe Gitlab::Metrics::Sampler do
describe '#flush' do
it 'schedules the metrics using Sidekiq' do
- expect(MetricsWorker).to receive(:perform_async).
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
with([an_instance_of(Hash)])
sampler.sample_memory_usage
@@ -51,8 +51,8 @@ describe Gitlab::Metrics::Sampler do
expect(Gitlab::Metrics::System).to receive(:memory_usage).
and_return(9000)
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('memory_usage', value: 9000).
+ expect(sampler).to receive(:add_metric).
+ with(/memory_usage/, value: 9000).
and_call_original
sampler.sample_memory_usage
@@ -64,8 +64,8 @@ describe Gitlab::Metrics::Sampler do
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count).
and_return(4)
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('file_descriptors', value: 4).
+ expect(sampler).to receive(:add_metric).
+ with(/file_descriptors/, value: 4).
and_call_original
sampler.sample_file_descriptors
@@ -74,8 +74,8 @@ describe Gitlab::Metrics::Sampler do
describe '#sample_objects' do
it 'adds a metric containing the amount of allocated objects' do
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('object_counts', an_instance_of(Hash), an_instance_of(Hash)).
+ expect(sampler).to receive(:add_metric).
+ with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)).
at_least(:once).
and_call_original
@@ -87,11 +87,53 @@ describe Gitlab::Metrics::Sampler do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
- expect(Gitlab::Metrics::Metric).to receive(:new).
- with('gc_statistics', an_instance_of(Hash)).
+ expect(sampler).to receive(:add_metric).
+ with(/gc_statistics/, an_instance_of(Hash)).
and_call_original
sampler.sample_gc
end
end
+
+ describe '#add_metric' do
+ it 'prefixes the series name for a Rails process' do
+ expect(sampler).to receive(:sidekiq?).and_return(false)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('rails_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+
+ it 'prefixes the series name for a Sidekiq process' do
+ expect(sampler).to receive(:sidekiq?).and_return(true)
+
+ expect(Gitlab::Metrics::Metric).to receive(:new).
+ with('sidekiq_cats', { value: 10 }, {}).
+ and_call_original
+
+ sampler.add_metric('cats', value: 10)
+ end
+ end
+
+ describe '#sleep_interval' do
+ it 'returns a Numeric' do
+ expect(sampler.sleep_interval).to be_a_kind_of(Numeric)
+ end
+
+ # Testing random behaviour is very hard, so treat this test as a basic smoke
+ # test instead of a very accurate behaviour/unit test.
+ it 'does not return the same interval twice in a row' do
+ last = nil
+
+ 100.times do
+ interval = sampler.sleep_interval
+
+ expect(interval).to_not eq(last)
+
+ last = interval
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
index 05214efc565..e520a968999 100644
--- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb
@@ -5,30 +5,15 @@ describe Gitlab::Metrics::SidekiqMiddleware do
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
+ worker = double(:worker, class: double(:class, name: 'TestWorker'))
- it 'does not track jobs of the MetricsWorker' do
- worker = MetricsWorker.new
+ expect(Gitlab::Metrics::Transaction).to receive(:new).
+ with('TestWorker#perform').
+ and_call_original
- expect(Gitlab::Metrics::Transaction).to_not receive(: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
index c6cd584663f..0695c5ce096 100644
--- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb
@@ -14,19 +14,15 @@ describe Gitlab::Metrics::Subscribers::ActionView do
before do
allow(subscriber).to receive(:current_transaction).and_return(transaction)
-
- allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
- and_return(['app/views/x.html.haml', 4])
end
describe '#render_template' do
it 'tracks rendering of a template' do
values = { duration: 2.1 }
- tags = {
- view: 'app/views/x.html.haml',
- file: 'app/views/x.html.haml',
- line: 4
- }
+ tags = { view: 'app/views/x.html.haml' }
+
+ expect(transaction).to receive(:increment).
+ with(:view_duration, 2.1)
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, values, tags)
diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
index 05b6cc14716..7bc070a4d09 100644
--- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
+++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb
@@ -2,31 +2,34 @@ require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:transaction) { Gitlab::Metrics::Transaction.new }
-
- let(:subscriber) { described_class.new }
+ let(:subscriber) { described_class.new }
let(:event) do
double(:event, duration: 0.2,
payload: { sql: 'SELECT * FROM users WHERE id = 10' })
end
- before do
- allow(subscriber).to receive(:current_transaction).and_return(transaction)
+ describe '#sql' do
+ describe 'without a current transaction' do
+ it 'simply returns' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ to_not receive(:increment)
- allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
- and_return(['app/models/foo.rb', 4])
- end
+ subscriber.sql(event)
+ end
+ end
- describe '#sql' do
- it 'tracks the execution of a SQL query' do
- sql = 'SELECT * FROM users WHERE id = ?'
- values = { duration: 0.2 }
- tags = { sql: sql, file: 'app/models/foo.rb', line: 4 }
+ describe 'with a current transaction' do
+ it 'increments the :sql_duration value' do
+ expect(subscriber).to receive(:current_transaction).
+ at_least(:once).
+ and_return(transaction)
- expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, values, tags)
+ expect(transaction).to receive(:increment).
+ with(:sql_duration, 0.2)
- subscriber.sql(event)
+ subscriber.sql(event)
+ end
end
end
end
diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb
index 5f17ff8ee75..1d5a51a157e 100644
--- a/spec/lib/gitlab/metrics/transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/transaction_spec.rb
@@ -11,6 +11,14 @@ describe Gitlab::Metrics::Transaction do
end
end
+ describe '#allocated_memory' do
+ it 'returns the allocated memory in bytes' do
+ transaction.run { 'a' * 32 }
+
+ expect(transaction.allocated_memory).to be_a_kind_of(Numeric)
+ end
+ end
+
describe '#run' do
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
@@ -30,14 +38,45 @@ describe Gitlab::Metrics::Transaction do
end
describe '#add_metric' do
- it 'adds a metric tagged with the transaction UUID' do
+ it 'adds a metric to the transaction' do
expect(Gitlab::Metrics::Metric).to receive(:new).
- with('foo', { number: 10 }, { transaction_id: transaction.uuid })
+ with('rails_foo', { number: 10 }, {})
transaction.add_metric('foo', number: 10)
end
end
+ describe '#increment' do
+ it 'increments a counter' do
+ transaction.increment(:time, 1)
+ transaction.increment(:time, 2)
+
+ values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) }
+
+ expect(transaction).to receive(:add_metric).
+ with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
+ describe '#set' do
+ it 'sets a value' do
+ transaction.set(:number, 10)
+
+ values = {
+ duration: 0.0,
+ number: 10,
+ allocated_memory: a_kind_of(Numeric)
+ }
+
+ expect(transaction).to receive(:add_metric).
+ with('transactions', values, {})
+
+ transaction.track_self
+ end
+ end
+
describe '#add_tag' do
it 'adds a tag' do
transaction.add_tag(:foo, 'bar')
@@ -57,8 +96,13 @@ describe Gitlab::Metrics::Transaction do
describe '#track_self' do
it 'adds a metric for the transaction itself' do
+ values = {
+ duration: transaction.duration,
+ allocated_memory: a_kind_of(Numeric)
+ }
+
expect(transaction).to receive(:add_metric).
- with(described_class::SERIES, { duration: transaction.duration }, {})
+ with('transactions', values, {})
transaction.track_self
end
@@ -68,10 +112,27 @@ describe Gitlab::Metrics::Transaction do
it 'submits the metrics to Sidekiq' do
transaction.track_self
- expect(MetricsWorker).to receive(:perform_async).
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
with([an_instance_of(Hash)])
transaction.submit
end
+
+ it 'adds the action as a tag for every metric' do
+ transaction.action = 'Foo#bar'
+ transaction.track_self
+
+ hash = {
+ series: 'rails_transactions',
+ tags: { action: 'Foo#bar' },
+ values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) },
+ timestamp: an_instance_of(Fixnum)
+ }
+
+ expect(Gitlab::Metrics).to receive(:submit_metrics).
+ with([hash])
+
+ transaction.submit
+ end
end
end
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 944642909aa..0ec8a6dc5cb 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -1,15 +1,9 @@
require 'spec_helper'
describe Gitlab::Metrics do
- describe '.pool_size' do
- it 'returns a Fixnum' do
- expect(described_class.pool_size).to be_an_instance_of(Fixnum)
- end
- end
-
- describe '.timeout' do
- it 'returns a Fixnum' do
- expect(described_class.timeout).to be_an_instance_of(Fixnum)
+ describe '.settings' do
+ it 'returns a Hash' do
+ expect(described_class.settings).to be_an_instance_of(Hash)
end
end
@@ -19,18 +13,51 @@ describe Gitlab::Metrics do
end
end
- describe '.hostname' do
- it 'returns a String containing the hostname' do
- expect(described_class.hostname).to eq(Socket.gethostname)
+ 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 '.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
+ describe '#escape_value' do
+ it 'escapes an equals sign' do
+ expect(described_class.escape_value('foo=')).to eq('foo\\=')
+ end
- expect(line).to eq(__LINE__ - 2)
- expect(file).to eq('spec/lib/gitlab/metrics_spec.rb')
+ it 'casts values to Strings' do
+ expect(described_class.escape_value(10)).to eq('10')
end
end
end
diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb
new file mode 100644
index 00000000000..eb433c38873
--- /dev/null
+++ b/spec/mailers/abuse_report_mailer_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe AbuseReportMailer do
+ include EmailSpec::Matchers
+
+ describe '.notify' do
+ context 'with admin_notification_email set' do
+ before do
+ stub_application_setting(admin_notification_email: 'admin@example.com')
+ end
+
+ it 'sends to the admin_notification_email' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to deliver_to 'admin@example.com'
+ end
+
+ it 'includes the user in the subject' do
+ report = create(:abuse_report)
+
+ mail = described_class.notify(report.id)
+
+ expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse"
+ end
+ end
+
+ context 'with no admin_notification_email set' do
+ it 'returns early' do
+ stub_application_setting(admin_notification_email: nil)
+
+ expect { described_class.notify(spy).deliver_now }.
+ not_to change { ActionMailer::Base.deliveries.count }
+ end
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 154901a2fbc..8f86c491d3f 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -104,6 +104,14 @@ describe Notify do
it { is_expected.to have_body_text /View Commit/ }
end
+ shared_examples 'an unsubscribeable thread' do
+ it { is_expected.to have_body_text /unsubscribe/ }
+ end
+
+ shared_examples "a user cannot unsubscribe through footer link" do
+ it { is_expected.not_to have_body_text /unsubscribe/ }
+ end
+
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) }
@@ -115,6 +123,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_behaves_like 'a user cannot unsubscribe through footer link'
it 'contains the password text' do
is_expected.to have_body_text /Click here to set your password/
@@ -134,7 +143,6 @@ describe Notify do
end
end
-
describe 'for users that signed up, the email' do
let(:example_site_path) { root_path }
let(:new_user) { create(:user, email: new_user_address, password: "securePassword") }
@@ -144,6 +152,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_behaves_like 'a user cannot unsubscribe through footer link'
it 'should not contain the new user\'s password' do
is_expected.not_to have_body_text /password/
@@ -157,6 +166,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do
is_expected.to deliver_to key.user.email
@@ -181,6 +191,7 @@ describe Notify do
subject { Notify.new_email_email(email.id) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like 'a user cannot unsubscribe through footer link'
it 'is sent to the new user' do
is_expected.to deliver_to email.user.email
@@ -227,6 +238,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/
@@ -253,6 +265,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -283,6 +296,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -319,6 +333,7 @@ describe Notify do
it_behaves_like 'an assignee email'
it_behaves_like 'an email starting a new thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -345,6 +360,7 @@ describe Notify do
subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) }
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'contains the description' do
is_expected.to have_body_text /#{merge_request_with_description.description}/
@@ -357,6 +373,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -387,6 +404,7 @@ describe Notify do
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -417,6 +435,7 @@ describe Notify do
it_behaves_like 'a multiple recipients email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like "an unsubscribeable thread"
it 'is sent as the merge author' do
sender = subject.header[:from].addrs[0]
@@ -446,6 +465,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Project was moved/
@@ -468,6 +488,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to project was granted/
@@ -518,6 +539,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'commit'
it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/
@@ -538,6 +560,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'merge_request'
it_behaves_like 'it should show Gmail Actions View Merge request link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
@@ -558,6 +581,7 @@ describe Notify do
it_behaves_like 'a note email'
it_behaves_like 'an answer to an existing thread', 'issue'
it_behaves_like 'it should show Gmail Actions View Issue link'
+ it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/
@@ -579,6 +603,7 @@ describe Notify do
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject' do
is_expected.to have_subject /Access to group was granted/
@@ -607,6 +632,7 @@ describe Notify do
subject { ActionMailer::Base.deliveries.last }
it_behaves_like 'an email sent from GitLab'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent to the new user' do
is_expected.to deliver_to 'new-email@mail.com'
@@ -629,6 +655,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -657,6 +684,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -684,6 +712,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -707,6 +736,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -734,6 +764,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -839,6 +870,7 @@ describe Notify do
subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
it_behaves_like 'it should show Gmail Actions View Commit link'
+ it_behaves_like "a user cannot unsubscribe through footer link"
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index d45319b25d4..f9be8fcbcfe 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -28,4 +28,37 @@ 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 '#remove_user' do
+ it 'blocks the user' do
+ report = build(:abuse_report)
+
+ allow(report.user).to receive(:destroy)
+
+ expect { report.remove_user }.to change { report.user.blocked? }.to(true)
+ end
+
+ it 'removes the user' do
+ report = build(:abuse_report)
+
+ expect { report.remove_user }.to change { User.count }.by(-1)
+ end
+ end
+
+ describe '#notify' do
+ it 'delivers' do
+ expect(AbuseReportMailer).to receive(:notify).with(subject.id).
+ and_return(spy)
+
+ subject.notify
+ end
+
+ it 'returns early when not persisted' do
+ report = build(:abuse_report)
+
+ expect(AbuseReportMailer).not_to receive(:notify)
+
+ report.notify
+ end
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index 35d8220ae54..91b250265e6 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -2,32 +2,45 @@
#
# Table name: application_settings
#
-# id :integer not null, primary key
-# default_projects_limit :integer
-# signup_enabled :boolean
-# signin_enabled :boolean
-# gravatar_enabled :boolean
-# sign_in_text :text
-# created_at :datetime
-# updated_at :datetime
-# home_page_url :string(255)
-# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
-# restricted_visibility_levels :text
-# version_check_enabled :boolean default(TRUE)
-# max_attachment_size :integer default(10), not null
-# default_project_visibility :integer
-# default_snippet_visibility :integer
-# restricted_signup_domains :text
-# user_oauth_applications :boolean default(TRUE)
-# after_sign_out_path :string(255)
-# session_expire_delay :integer default(10080), not null
-# import_sources :text
-# help_page_text :text
-# admin_notification_email :string(255)
-# shared_runners_enabled :boolean default(TRUE), not null
-# max_artifacts_size :integer default(100), not null
-# runners_registration_token :string(255)
+# id :integer not null, primary key
+# default_projects_limit :integer
+# signup_enabled :boolean
+# signin_enabled :boolean
+# gravatar_enabled :boolean
+# sign_in_text :text
+# created_at :datetime
+# updated_at :datetime
+# home_page_url :string(255)
+# default_branch_protection :integer default(2)
+# twitter_sharing_enabled :boolean default(TRUE)
+# restricted_visibility_levels :text
+# version_check_enabled :boolean default(TRUE)
+# max_attachment_size :integer default(10), not null
+# default_project_visibility :integer
+# default_snippet_visibility :integer
+# restricted_signup_domains :text
+# user_oauth_applications :boolean default(TRUE)
+# after_sign_out_path :string(255)
+# session_expire_delay :integer default(10080), not null
+# import_sources :text
+# help_page_text :text
+# admin_notification_email :string(255)
+# shared_runners_enabled :boolean default(TRUE), not null
+# max_artifacts_size :integer default(100), not null
+# runners_registration_token :string
+# require_two_factor_authentication :boolean default(FALSE)
+# two_factor_grace_period :integer default(48)
+# metrics_enabled :boolean default(FALSE)
+# metrics_host :string default("localhost")
+# metrics_username :string
+# metrics_password :string
+# metrics_pool_size :integer default(16)
+# metrics_timeout :integer default(10)
+# metrics_method_call_threshold :integer default(10)
+# recaptcha_enabled :boolean default(FALSE)
+# recaptcha_site_key :string
+# recaptcha_private_key :string
+# metrics_port :integer default(8089)
#
require 'spec_helper'
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index e4cac105110..f6f84db57e6 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -6,7 +6,6 @@
# message :text not null
# starts_at :datetime
# ends_at :datetime
-# alert_type :integer
# created_at :datetime
# updated_at :datetime
# color :string(255)
@@ -16,6 +15,8 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
+ include ActiveSupport::Testing::TimeHelpers
+
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
@@ -35,20 +36,79 @@ describe BroadcastMessage, models: true do
it { is_expected.not_to allow_value('000').for(:font) }
end
- describe :current do
+ describe '.current' do
it "should return last message if time match" do
- broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow)
- expect(BroadcastMessage.current).to eq(broadcast_message)
+ message = create(:broadcast_message)
+
+ expect(BroadcastMessage.current).to eq message
end
it "should return nil if time not come" do
- create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days)
+ create(:broadcast_message, :future)
+
expect(BroadcastMessage.current).to be_nil
end
it "should return nil if time has passed" do
- create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday)
+ create(:broadcast_message, :expired)
+
expect(BroadcastMessage.current).to be_nil
end
end
+
+ describe '#active?' do
+ it 'is truthy when started and not ended' do
+ message = build(:broadcast_message)
+
+ expect(message).to be_active
+ end
+
+ it 'is falsey when ended' do
+ message = build(:broadcast_message, :expired)
+
+ expect(message).not_to be_active
+ end
+
+ it 'is falsey when not started' do
+ message = build(:broadcast_message, :future)
+
+ expect(message).not_to be_active
+ end
+ end
+
+ describe '#started?' do
+ it 'is truthy when starts_at has passed' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.from_now) do
+ expect(message).to be_started
+ end
+ end
+
+ it 'is falsey when starts_at is in the future' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.ago) do
+ expect(message).not_to be_started
+ end
+ end
+ end
+
+ describe '#ended?' do
+ it 'is truthy when ends_at has passed' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.from_now) do
+ expect(message).to be_ended
+ end
+ end
+
+ it 'is falsey when ends_at is in the future' do
+ message = build(:broadcast_message)
+
+ travel_to(3.days.ago) do
+ expect(message).not_to be_ended
+ end
+ end
+ end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 1c22e3cb7c4..0e13456723d 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -1,28 +1,3 @@
-# == Schema Information
-#
-# Table name: builds
-#
-# id :integer not null, primary key
-# project_id :integer
-# status :string(255)
-# finished_at :datetime
-# trace :text
-# created_at :datetime
-# updated_at :datetime
-# started_at :datetime
-# runner_id :integer
-# commit_id :integer
-# coverage :float
-# commands :text
-# job_id :integer
-# name :string(255)
-# deploy :boolean default(FALSE)
-# options :text
-# allow_failure :boolean default(FALSE), not null
-# stage :string(255)
-# trigger_request_id :integer
-#
-
require 'spec_helper'
describe Ci::Build, models: true do
@@ -368,21 +343,75 @@ describe Ci::Build, models: true do
end
end
- describe :download_url do
- subject { build.download_url }
+ describe :artifacts_download_url do
+ subject { build.artifacts_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
+ it 'should not 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 :artifacts_browse_url do
+ subject { build.artifacts_browse_url }
+
+ it "should be nil if artifacts browser is unsupported" do
+ allow(build).to receive(:artifacts_browser_supported?).and_return(false)
+ is_expected.to be_nil
+ end
+
+ it 'should not be nil if artifacts browser is supported' do
+ allow(build).to receive(:artifacts_browser_supported?).and_return(true)
+ is_expected.to_not be_nil
+ end
+ end
+
+ describe :artifacts? do
+ subject { build.artifacts? }
+
+ context 'artifacts archive does not exist' do
+ before { build.update_attributes(artifacts_file: nil) }
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive exists' do
+ before do
+ gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif')
+ build.update_attributes(artifacts_file: gif)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+
+ describe :artifacts_browser_supported? do
+ subject { build.artifacts_browser_supported? }
+ context 'artifacts metadata does not exist' do
+ it { is_expected.to be_falsy }
+ end
+
+ context 'artifacts archive is a zip file and metadata exists' do
+ before do
+ fixture_dir = Rails.root + 'spec/fixtures/'
+ archive = fixture_file_upload(fixture_dir + 'ci_build_artifacts.zip',
+ 'application/zip')
+ metadata = fixture_file_upload(fixture_dir + 'ci_build_artifacts_metadata.gz',
+ 'application/x-gzip')
+ build.update_attributes(artifacts_file: archive)
+ build.update_attributes(artifacts_metadata: metadata)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
describe :repo_url do
let(:build) { FactoryGirl.create :ci_build }
let(:project) { build.project }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
new file mode 100644
index 00000000000..36d10636ae9
--- /dev/null
+++ b/spec/models/ci/build_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Ci::Build, models: true do
+ let(:build) { create(:ci_build) }
+ let(:test_trace) { 'This is a test' }
+
+ describe '#trace' do
+ it 'obfuscates project runners token' do
+ allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}")
+
+ expect(build.trace).to eq("Test: xxxxxx")
+ end
+
+ it 'empty project runners token' do
+ allow(build).to receive(:raw_trace).and_return(test_trace)
+ # runners_token can't normally be set to nil
+ allow(build.project).to receive(:runners_token).and_return(nil)
+
+ expect(build.trace).to eq(test_trace)
+ end
+ end
+end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
index b193e16e7f8..dfc0cc3be1c 100644
--- a/spec/models/ci/commit_spec.rb
+++ b/spec/models/ci/commit_spec.rb
@@ -13,7 +13,7 @@
# tag :boolean default(FALSE)
# yaml_errors :text
# committed_at :datetime
-# project_id :integer
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
index da8491357a5..000a732db77 100644
--- a/spec/models/ci/runner_project_spec.rb
+++ b/spec/models/ci/runner_project_spec.rb
@@ -2,11 +2,12 @@
#
# Table name: ci_runner_projects
#
-# id :integer not null, primary key
-# runner_id :integer not null
-# project_id :integer not null
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# runner_id :integer not null
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index cb2f51e2011..159be939300 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -2,12 +2,13 @@
#
# Table name: ci_triggers
#
-# id :integer not null, primary key
-# token :string(255)
-# project_id :integer not null
-# deleted_at :datetime
-# created_at :datetime
-# updated_at :datetime
+# id :integer not null, primary key
+# token :string(255)
+# project_id :integer
+# deleted_at :datetime
+# created_at :datetime
+# updated_at :datetime
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 31b56953a13..71e84091cb7 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -3,12 +3,13 @@
# Table name: ci_variables
#
# id :integer not null, primary key
-# project_id :integer not null
+# project_id :integer
# key :string(255)
# value :text
# encrypted_value :text
# encrypted_value_salt :string(255)
# encrypted_value_iv :string(255)
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index b8f901b3433..82c68ff6cb1 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index f9d3c56750f..021d62cdf0c 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -99,4 +99,18 @@ describe Issue, "Issuable" do
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/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb
index b198aa77526..d37978720bf 100644
--- a/spec/models/external_wiki_service_spec.rb
+++ b/spec/models/external_wiki_service_spec.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'spec_helper'
diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb
index d61c1c96bde..5b0883d8702 100644
--- a/spec/models/generic_commit_status_spec.rb
+++ b/spec/models/generic_commit_status_spec.rb
@@ -29,6 +29,7 @@
# target_url :string(255)
# description :string(255)
# artifacts_file :text
+# gl_project_id :integer
#
require 'spec_helper'
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 646f767e6fe..3c995053eec 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'spec_helper'
@@ -38,14 +37,6 @@ describe Group, models: true do
it { is_expected.not_to validate_presence_of :owner }
end
- describe '.public_and_given_groups' do
- let!(:public_group) { create(:group, public: true) }
-
- subject { described_class.public_and_given_groups([group.id]) }
-
- it { is_expected.to eq([public_group, group]) }
- end
-
describe '.visible_to_user' do
let!(:group) { create(:group) }
let!(:user) { create(:user) }
@@ -112,23 +103,4 @@ describe Group, models: true do
expect(group.avatar_type).to eq(["only images allowed"])
end
end
-
- describe "public_profile?" do
- it "returns true for public group" do
- group = create(:group, public: true)
- expect(group.public_profile?).to be_truthy
- end
-
- it "returns true for non-public group with public project" do
- group = create(:group)
- create(:project, :public, group: group)
- expect(group.public_profile?).to be_truthy
- end
-
- it "returns false for non-public group with no public projects" do
- group = create(:group)
- create(:project, group: group)
- expect(group.public_profile?).to be_falsy
- end
- end
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 2d90b0793cc..7070aa4ac62 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -77,5 +77,17 @@ describe ProjectHook, models: true do
expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error'])
end
+
+ it "handles 200 status code" do
+ WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success")
+
+ expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success'])
+ end
+
+ it "handles 2xx status codes" do
+ WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success")
+
+ expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success'])
+ end
end
end
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
new file mode 100644
index 00000000000..5afe042e154
--- /dev/null
+++ b/spec/models/identity_spec.rb
@@ -0,0 +1,38 @@
+# == Schema Information
+#
+# Table name: identities
+#
+# id :integer not null, primary key
+# extern_uid :string(255)
+# provider :string(255)
+# user_id :integer
+# created_at :datetime
+# updated_at :datetime
+#
+
+require 'spec_helper'
+
+RSpec.describe Identity, models: true do
+
+ describe 'relations' do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'fields' do
+ it { is_expected.to respond_to(:provider) }
+ it { is_expected.to respond_to(:extern_uid) }
+ end
+
+ describe '#is_ldap?' do
+ let(:ldap_identity) { create(:identity, provider: 'ldapmain') }
+ let(:other_identity) { create(:identity, provider: 'twitter') }
+
+ it 'returns true if it is a ldap identity' do
+ expect(ldap_identity.ldap?).to be_truthy
+ end
+
+ it 'returns false if it is not a ldap identity' do
+ expect(other_identity.ldap?).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index e0653a8327d..291e6200a5b 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2,25 +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
-# merge_error :string(255)
+# id :integer not null, primary key
+# target_branch :string(255) not null
+# source_branch :string(255) not null
+# source_project_id :integer not null
+# author_id :integer
+# assignee_id :integer
+# title :string(255)
+# created_at :datetime
+# updated_at :datetime
+# milestone_id :integer
+# state :string(255)
+# merge_status :string(255)
+# target_project_id :integer not null
+# iid :integer
+# description :text
+# position :integer default(0)
+# locked_at :datetime
+# updated_by_id :integer
+# merge_error :string(255)
+# merge_params :text
+# merge_when_build_succeeds :boolean default(FALSE), not null
+# merge_user_id :integer
#
require 'spec_helper'
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 4fa2d2bc4d2..e0b3290e416 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -11,7 +11,6 @@
# type :string(255)
# description :string(255) default(""), not null
# avatar :string(255)
-# public :boolean default(FALSE)
#
require 'spec_helper'
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 593d8f76215..9182b42661d 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -125,6 +125,19 @@ describe Note, models: true do
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
end
+ describe "#all_references" do
+ let!(:note1) { create(:note) }
+ let!(:note2) { create(:note) }
+
+ it "reads the rendered note body from the cache" do
+ expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project)
+ expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project)
+
+ note1.all_references
+ note2.all_references
+ end
+ end
+
describe :search do
let!(:note) { create(:note, note: "WoW") }
@@ -164,7 +177,31 @@ describe Note, models: true do
expect(note.editable?).to be_falsy
end
end
-
+
+ describe "cross_reference_not_visible_for?" do
+ let(:private_user) { create(:user) }
+ let(:private_project) { create(:project, namespace: private_user.namespace).tap { |p| p.team << [private_user, :master] } }
+ let(:private_issue) { create(:issue, project: private_project) }
+
+ let(:ext_proj) { create(:project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+
+ let(:note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ system: true
+ end
+
+ it "returns true" do
+ expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy
+ end
+
+ it "returns false" do
+ expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy
+ end
+ end
+
describe "set_award!" do
let(:issue) { create :issue }
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 64bb92fba95..f3d15f3c1ea 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -40,6 +40,20 @@ describe AsanaService, models: true do
let(:user) { create(:user) }
let(:project) { create(:project) }
+ def create_data_for_commits(*messages)
+ {
+ object_kind: 'push',
+ ref: 'master',
+ user_name: user.name,
+ commits: messages.map do |m|
+ {
+ message: m,
+ url: 'https://gitlab.com/',
+ }
+ end
+ }
+ end
+
before do
@asana = AsanaService.new
allow(@asana).to receive_messages(
@@ -51,16 +65,67 @@ describe AsanaService, models: true do
)
end
- it 'should call Asana service to created a story' do
- expect(Asana::Task).to receive(:find).with('123456').once
+ it 'should call Asana service to create a story' do
+ data = create_data_for_commits('Message from commit. related to #123456')
+ expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
- @asana.check_commit('related to #123456', 'pushed')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment).with(text: expected_message)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1)
+
+ @asana.execute(data)
end
- it 'should call Asana service to created a story and close a task' do
- expect(Asana::Task).to receive(:find).with('456789').twice
+ it 'should call Asana service to create a story and close a task' do
+ data = create_data_for_commits('fix #456789')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'should be able to close via url' do
+ data = create_data_for_commits('closes https://app.asana.com/19292/956299/42')
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1)
+
+ @asana.execute(data)
+ end
+
+ it 'should allow multiple matches per line' do
+ message = <<-EOF
+ minor bigfix, refactoring, fixed #123 and Closes #456 work on #789
+ ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12
+ EOF
+ data = create_data_for_commits(message)
+ d1 = double('Asana::Task')
+ expect(d1).to receive(:add_comment)
+ expect(d1).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1)
+
+ d2 = double('Asana::Task')
+ expect(d2).to receive(:add_comment)
+ expect(d2).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2)
+
+ d3 = double('Asana::Task')
+ expect(d3).to receive(:add_comment)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3)
+
+ d4 = double('Asana::Task')
+ expect(d4).to receive(:add_comment)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4)
+
+ d5 = double('Asana::Task')
+ expect(d5).to receive(:add_comment)
+ expect(d5).to receive(:update).with(completed: true)
+ expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5)
- @asana.check_commit('fix #456789', 'pushed')
+ @asana.execute(data)
end
end
end
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
new file mode 100644
index 00000000000..905379a64e3
--- /dev/null
+++ b/spec/models/project_services/builds_email_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe BuildsEmailService do
+ let(:build) { create(:ci_build) }
+ let(:data) { Gitlab::BuildDataBuilder.build(build) }
+ let(:service) { BuildsEmailService.new }
+
+ describe :execute do
+ it "sends email" do
+ service.recipients = 'test@gitlab.com'
+ data[:build_status] = 'failed'
+ expect(BuildEmailWorker).to receive(:perform_async)
+ service.execute(data)
+ end
+
+ it "does not sends email with failed build and allowed_failure on" do
+ data[:build_status] = 'failed'
+ data[:build_allow_failure] = true
+ expect(BuildEmailWorker).not_to receive(:perform_async)
+ service.execute(data)
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 400bdf2d962..a3de23369e1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,6 +29,13 @@
# import_source :string(255)
# commit_count :integer default(0)
# import_error :text
+# ci_id :integer
+# builds_enabled :boolean default(TRUE), not null
+# shared_runners_enabled :boolean default(TRUE), not null
+# runners_token :string
+# build_coverage_regex :string
+# build_allow_git_fetch :boolean default(TRUE), not null
+# build_timeout :integer default(3600), not null
#
require 'spec_helper'
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 876b927eaea..a2085df5bcd 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -36,6 +36,13 @@ describe ProjectWiki, models: true do
end
end
+ describe "#wiki_base_path" do
+ it "returns the wiki base path" do
+ wiki_base_path = "/#{project.path_with_namespace}/wikis"
+ expect(subject.wiki_base_path).to eq(wiki_base_path)
+ end
+ end
+
describe "#wiki" do
it "contains a Gollum::Wiki instance" do
expect(subject.wiki).to be_a Gollum::Wiki
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 0ca82365b98..173628c08d0 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -16,6 +16,7 @@
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
#
require 'spec_helper'
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 2f184bbaf92..0bef68e2885 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,62 +2,63 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# unlock_token :string(255)
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
-# consumed_timestep :integer
-# layout :integer default(0)
-# hide_project_limit :boolean default(FALSE)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
+# consumed_timestep :integer
+# layout :integer default(0)
+# hide_project_limit :boolean default(FALSE)
+# unlock_token :string
+# otp_grace_period_started_at :datetime
#
require 'spec_helper'
@@ -106,7 +107,7 @@ describe User, models: true do
end
it 'validates uniqueness' do
- expect(subject).to validate_uniqueness_of(:username)
+ expect(subject).to validate_uniqueness_of(:username).case_insensitive
end
end
@@ -568,27 +569,39 @@ describe User, models: true do
end
end
- describe :ldap_user? do
- it "is true if provider name starts with ldap" do
- user = create(:omniauth_user, provider: 'ldapmain')
- expect( user.ldap_user? ).to be_truthy
- end
+ context 'ldap synchronized user' do
+ describe :ldap_user? do
+ it 'is true if provider name starts with ldap' do
+ user = create(:omniauth_user, provider: 'ldapmain')
+ expect(user.ldap_user?).to be_truthy
+ end
- it "is false for other providers" do
- user = create(:omniauth_user, provider: 'other-provider')
- expect( user.ldap_user? ).to be_falsey
+ it 'is false for other providers' do
+ user = create(:omniauth_user, provider: 'other-provider')
+ expect(user.ldap_user?).to be_falsey
+ end
+
+ it 'is false if no extern_uid is provided' do
+ user = create(:omniauth_user, extern_uid: nil)
+ expect(user.ldap_user?).to be_falsey
+ end
end
- it "is false if no extern_uid is provided" do
- user = create(:omniauth_user, extern_uid: nil)
- expect( user.ldap_user? ).to be_falsey
+ describe :ldap_identity do
+ it 'returns ldap identity' do
+ user = create :omniauth_user
+ expect(user.ldap_identity.provider).not_to be_empty
+ end
end
- end
- describe :ldap_identity do
- it "returns ldap identity" do
- user = create :omniauth_user
- expect(user.ldap_identity.provider).not_to be_empty
+ describe '#ldap_block' do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') }
+
+ it 'blocks user flaging the action caming from ldap' do
+ user.ldap_block
+ expect(user.blocked?).to be_truthy
+ expect(user.ldap_blocked?).to be_truthy
+ end
end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
new file mode 100644
index 00000000000..8c9f5a382b7
--- /dev/null
+++ b/spec/requests/api/builds_spec.rb
@@ -0,0 +1,172 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:developer) { create(:project_member, user: user, project: project, access_level: ProjectMember::DEVELOPER) }
+ let!(:reporter) { create(:project_member, user: user2, project: project, access_level: ProjectMember::REPORTER) }
+ let(:commit) { create(:ci_commit, project: project)}
+ let(:build) { create(:ci_build, commit: commit) }
+ let(:build_with_trace) { create(:ci_build_with_trace, commit: commit) }
+ let(:build_canceled) { create(:ci_build, :canceled, commit: commit) }
+
+ describe 'GET /projects/:id/builds ' do
+ context 'authorized user' do
+ it 'should return project builds' do
+ get api("/projects/#{project.id}/builds", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'should filter project with one scope element' do
+ get api("/projects/#{project.id}/builds?scope=pending", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'should filter project with array of scope elements' do
+ get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=running", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+
+ it 'should respond 400 when scope contains invalid state' do
+ get api("/projects/#{project.id}/builds?scope[0]=pending&scope[1]=unknown_status", user)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project builds' do
+ get api("/projects/#{project.id}/builds")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/repository/commits/:sha/builds' do
+ context 'authorized user' do
+ it 'should return project builds for specific commit' do
+ project.ensure_ci_commit(commit.sha)
+ get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project builds' do
+ project.ensure_ci_commit(commit.sha)
+ get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id' do
+ context 'authorized user' do
+ it 'should return specific build data' do
+ get api("/projects/#{project.id}/builds/#{build.id}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq('test')
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return specific build data' do
+ get api("/projects/#{project.id}/builds/#{build.id}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/builds/:build_id/trace' do
+ context 'authorized user' do
+ it 'should return specific build trace' do
+ get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace", user)
+
+ expect(response.status).to eq(200)
+ expect(response.body).to eq(build_with_trace.trace)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return specific build trace' do
+ get api("/projects/#{project.id}/builds/#{build_with_trace.id}/trace")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/cancel' do
+ context 'authorized user' do
+ context 'user with :manage_builds persmission' do
+ it 'should cancel running or pending build' do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel", user)
+
+ expect(response.status).to eq(201)
+ expect(project.builds.first.status).to eq('canceled')
+ end
+ end
+
+ context 'user without :manage_builds permission' do
+ it 'should not cancel build' do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not cancel build' do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/builds/:build_id/retry' do
+ context 'authorized user' do
+ context 'user with :manage_builds persmission' do
+ it 'should retry non-running build' do
+ post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user)
+
+ expect(response.status).to eq(201)
+ expect(project.builds.first.status).to eq('canceled')
+ expect(json_response['status']).to eq('pending')
+ end
+ end
+
+ context 'user without :manage_builds permission' do
+ it 'should not retry build' do
+ post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not retry build' do
+ post api("/projects/#{project.id}/builds/#{build_canceled.id}/retry")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb
index a28607bd240..21482fc1070 100644
--- a/spec/requests/api/commit_status_spec.rb
+++ b/spec/requests/api/commit_status_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe API::API, api: true do
+describe API::CommitStatus, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -12,6 +12,10 @@ describe API::API, api: true do
let(:commit_status) { create(:commit_status, commit: ci_commit) }
describe "GET /projects/:id/repository/commits/:sha/statuses" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) }
+ end
+
context "reporter user" do
let(:statuses_id) { json_response.map { |status| status['id'] } }
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 8b177af4689..39f9a06fe1b 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -10,9 +10,32 @@ describe API::API, api: true do
let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) }
let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) }
let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) }
+
+ # For testing the cross-reference of a private issue in a public issue
+ let(:private_user) { create(:user) }
+ let(:private_project) do
+ create(:project, namespace: private_user.namespace).
+ tap { |p| p.team << [private_user, :master] }
+ end
+ let(:private_issue) { create(:issue, project: private_project) }
+
+ let(:ext_proj) { create(:project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+
+ let!(:cross_reference_note) do
+ create :note,
+ noteable: ext_issue, project: ext_proj,
+ note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ system: true
+ end
+
before { project.team << [user, :reporter] }
describe "GET /projects/:id/noteable/:noteable_id/notes" do
+ it_behaves_like 'a paginated resources' do
+ let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) }
+ end
+
context "when noteable is an Issue" do
it "should return an array of issue notes" do
get api("/projects/#{project.id}/issues/#{issue.id}/notes", user)
@@ -25,6 +48,24 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/123/notes", user)
expect(response.status).to eq(404)
end
+
+ context "that references a private issue" do
+ it "should return an empty array" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response).to be_empty
+ end
+
+ context "and current user can view the note" do
+ it "should return an empty array" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
end
context "when noteable is a Snippet" do
@@ -68,6 +109,21 @@ describe API::API, api: true do
get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user)
expect(response.status).to eq(404)
end
+
+ context "that references a private issue" do
+ it "should return a 404 error" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user)
+ expect(response.status).to eq(404)
+ end
+
+ context "and current user can view the note" do
+ it "should return an issue note by id" do
+ get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user)
+ expect(response.status).to eq(200)
+ expect(json_response['body']).to eq(cross_reference_note.note)
+ end
+ end
+ end
end
context "when noteable is a Snippet" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 7f0f9454b10..6f4c336b66c 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -353,6 +353,20 @@ describe API::API, api: true do
end
end
+ describe "POST /projects/:id/uploads" do
+ before { project }
+
+ it "uploads the file and returns its info" do
+ post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")
+
+ expect(response.status).to be(201)
+ expect(json_response['alt']).to eq("dk")
+ expect(json_response['url']).to start_with("/uploads/")
+ expect(json_response['url']).to end_with("/dk.png")
+ expect(json_response['is_image']).to eq(true)
+ end
+ end
+
describe 'GET /projects/:id' do
before { project }
before { project_member }
@@ -382,6 +396,15 @@ describe API::API, api: true do
expect(response.status).to eq(404)
end
+ it 'should handle users with dots' do
+ dot_user = create(:user, username: 'dot.user')
+ project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace)
+
+ get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user)
+ expect(response.status).to eq(200)
+ expect(json_response['name']).to eq(project.name)
+ end
+
describe 'permissions' do
context 'all projects' do
it 'Contains permission information' do
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 17f2643fd45..f966e38cd3e 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -65,6 +65,27 @@ describe API::API, api: true do
end
end
+ describe 'DELETE /projects/:id/repository/tags/:tag_name' do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+
+ before do
+ allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true)
+ end
+
+ context 'delete tag' do
+ it 'should delete an existing tag' do
+ delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user)
+ expect(response.status).to eq(200)
+ expect(json_response['tag_name']).to eq(tag_name)
+ end
+
+ it 'should raise 404 if the tag does not exist' do
+ delete api("/projects/#{project.id}/repository/tags/foobar", user)
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
context 'annotated tag' do
it 'should create a new annotated tag' do
# Identity must be set in .gitconfig to create annotated tag.
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 314bd7ddc59..2a86b60bc4d 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -3,11 +3,19 @@ require 'spec_helper'
describe API::API do
include ApiHelpers
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:trigger_token) { 'secure_token' }
+ let!(:trigger_token_2) { 'secure_token_2' }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+ let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
+ let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
+ let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) }
+ let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') }
+
describe 'POST /projects/:project_id/trigger' do
- let!(:trigger_token) { 'secure token' }
- let!(:project) { FactoryGirl.create(:project) }
- let!(:project2) { FactoryGirl.create(:empty_project) }
- let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
+ let!(:project2) { create(:empty_project) }
let(:options) do
{
token: trigger_token
@@ -77,4 +85,127 @@ describe API::API do
end
end
end
+
+ describe 'GET /projects/:id/triggers' do
+ context 'authenticated user with valid permissions' do
+ it 'should return list of triggers' do
+ get api("/projects/#{project.id}/triggers", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Array)
+ expect(json_response[0]).to have_key('token')
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'should return trigger details' do
+ get api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Hash)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ get api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not return triggers list' do
+ get api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/triggers' do
+ context 'authenticated user with valid permissions' do
+ it 'should create trigger' do
+ expect do
+ post api("/projects/#{project.id}/triggers", user)
+ end.to change{project.triggers.count}.by(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response).to be_a(Hash)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not create trigger' do
+ post api("/projects/#{project.id}/triggers", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not create trigger' do
+ post api("/projects/#{project.id}/triggers")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/triggers/:token' do
+ context 'authenticated user with valid permissions' do
+ it 'should delete trigger' do
+ expect do
+ delete api("/projects/#{project.id}/triggers/#{trigger.token}", user)
+ end.to change{project.triggers.count}.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing trigger' do
+ delete api("/projects/#{project.id}/triggers/abcdef012345", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authenticated user with invalid permissions' do
+ it 'should not delete trigger' do
+ delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthenticated user' do
+ it 'should not delete trigger' do
+ delete api("/projects/#{project.id}/triggers/#{trigger.token}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 4f278551d07..b82c5c7685f 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -8,6 +8,8 @@ describe API::API, api: true do
let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) }
let(:omniauth_user) { create(:omniauth_user) }
+ let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
+ let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
describe "GET /users" do
context "when unauthenticated" do
@@ -783,6 +785,12 @@ describe API::API, api: true do
expect(user.reload.state).to eq('blocked')
end
+ it 'should not re-block ldap blocked users' do
+ put api("/users/#{ldap_blocked_user.id}/block", admin)
+ expect(response.status).to eq(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
+ end
+
it 'should not be available for non admin users' do
put api("/users/#{user.id}/block", user)
expect(response.status).to eq(403)
@@ -797,7 +805,9 @@ describe API::API, api: true do
end
describe 'PUT /user/:id/unblock' do
+ let(:blocked_user) { create(:user, state: 'blocked') }
before { admin }
+
it 'should unblock existing user' do
put api("/users/#{user.id}/unblock", admin)
expect(response.status).to eq(200)
@@ -805,12 +815,15 @@ describe API::API, api: true do
end
it 'should unblock a blocked user' do
- put api("/users/#{user.id}/block", admin)
- expect(response.status).to eq(200)
- expect(user.reload.state).to eq('blocked')
- put api("/users/#{user.id}/unblock", admin)
+ put api("/users/#{blocked_user.id}/unblock", admin)
expect(response.status).to eq(200)
- expect(user.reload.state).to eq('active')
+ expect(blocked_user.reload.state).to eq('active')
+ end
+
+ it 'should not unblock ldap blocked users' do
+ put api("/users/#{ldap_blocked_user.id}/unblock", admin)
+ expect(response.status).to eq(403)
+ expect(ldap_blocked_user.reload.state).to eq('ldap_blocked')
end
it 'should not be available for non admin users' do
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
new file mode 100644
index 00000000000..9744729ba0c
--- /dev/null
+++ b/spec/requests/api/variables_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+ let!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
+ let!(:variable) { create(:ci_variable, project: project) }
+
+ describe 'GET /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variables' do
+ get api("/projects/#{project.id}/variables", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['value']).to eq(variable.value)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing variable' do
+ get api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should create variable' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+ end.to change{project.variables.count}.by(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ end
+
+ it 'should not allow to duplicate variable key' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
+ end.to change{project.variables.count}.by(0)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should update variable data' do
+ initial_variable = project.variables.first
+ value_before = initial_variable.value
+
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+
+ updated_variable = project.variables.first
+
+ expect(response.status).to eq(200)
+ expect(value_before).to eq(variable.value)
+ expect(updated_variable.value).to eq('VALUE_1_UP')
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ put api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should delete variable' do
+ expect do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+ end.to change{project.variables.count}.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ delete api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index c27e87c4acc..648ea0d5f50 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -210,6 +210,52 @@ describe Ci::API::API do
end
end
+ context 'should post artifacts file and metadata file' do
+ let!(:artifacts) { file_upload }
+ let!(:metadata) { file_upload2 }
+
+ let(:stored_artifacts_file) { build.reload.artifacts_file.file }
+ let(:stored_metadata_file) { build.reload.artifacts_metadata.file }
+
+ before do
+ build.run!
+ post(post_url, post_data, headers_with_token)
+ end
+
+ context 'post data accelerated by workhorse is correct' do
+ let(:post_data) do
+ { 'file.path' => artifacts.path,
+ 'file.name' => artifacts.original_filename,
+ 'metadata.path' => metadata.path,
+ 'metadata.name' => metadata.original_filename }
+ end
+
+ it 'responds with valid status' do
+ expect(response.status).to eq(201)
+ end
+
+ it 'stores artifacts and artifacts metadata' do
+ expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
+ expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
+ end
+ end
+
+ context 'no artifacts file in post data' do
+ let(:post_data) do
+ { 'metadata' => metadata }
+ end
+
+ it 'is expected to respond with bad request' do
+ expect(response.status).to eq(400)
+ end
+
+ it 'does not store metadata' do
+ expect(stored_metadata_file).to be_nil
+ end
+ end
+ end
+
+
context "should fail to post too large artifact" do
before do
build.run!
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 82f62a8709c..22ba25217f0 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -80,6 +80,7 @@ describe ProjectsController, 'routing' do
it 'to #show' do
expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq')
+ expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys')
end
it 'to #update' do
@@ -434,6 +435,18 @@ describe Projects::TreeController, 'routing' do
end
end
+# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/}
+# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/}
+describe Projects::FindFileController, 'routing' do
+ it 'to #show' do
+ expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
+ end
+
+ it 'to #list' do
+ expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json')
+ end
+end
+
describe Projects::BlobController, 'routing' do
it 'to #edit' do
expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to(
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index c103752198d..6d219f35895 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -52,6 +52,9 @@ describe NotificationService, services: true do
it do
add_users_with_subscription(note.project, issue)
+ # Ensure create SentNotification by noteable = issue 6 times, not noteable = note
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times
+
ActionMailer::Base.deliveries.clear
notification.new_note(note)
@@ -61,6 +64,7 @@ describe NotificationService, services: true do
should_email(note.noteable.assignee)
should_email(@u_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
should_not_email(note.author)
should_not_email(@u_participating)
@@ -245,6 +249,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -260,6 +265,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
end
@@ -282,6 +288,7 @@ describe NotificationService, services: true do
should_email(merge_request.assignee)
should_email(@u_watcher)
+ should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -296,6 +303,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -310,6 +318,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -324,6 +333,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -338,6 +348,7 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@watcher_and_subscriber)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -387,14 +398,18 @@ describe NotificationService, services: true do
@subscriber = create :user
@unsubscriber = create :user
@subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING)
+ @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH)
project.team << [@subscribed_participant, :master]
project.team << [@subscriber, :master]
project.team << [@unsubscriber, :master]
+ project.team << [@watcher_and_subscriber, :master]
issuable.subscriptions.create(user: @subscriber, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
+ # Make the watcher a subscriber to detect dupes
+ issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true)
end
def sent_to_user?(user)
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
index 5ceed5af9a5..f252e2c5902 100644
--- a/spec/services/projects/download_service_spec.rb
+++ b/spec/services/projects/download_service_spec.rb
@@ -33,12 +33,12 @@ describe Projects::DownloadService, services: true do
@link_to_file = download_file(@project, url)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
- it { expect(@link_to_file['is_image']).to be true }
- it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
- it { expect(@link_to_file['alt']).to eq('rails_sample') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
+ it { expect(@link_to_file[:is_image]).to be true }
+ it { expect(@link_to_file[:url]).to match('rails_sample.jpg') }
+ it { expect(@link_to_file[:alt]).to eq('rails_sample') }
end
context 'a txt file' do
@@ -47,12 +47,12 @@ describe Projects::DownloadService, services: true do
@link_to_file = download_file(@project, url)
end
- it { expect(@link_to_file).to have_key('alt') }
- it { expect(@link_to_file).to have_key('url') }
- it { expect(@link_to_file).to have_key('is_image') }
- it { expect(@link_to_file['is_image']).to be false }
- it { expect(@link_to_file['url']).to match('doc_sample.txt') }
- it { expect(@link_to_file['alt']).to eq('doc_sample.txt') }
+ it { expect(@link_to_file).to have_key(:alt) }
+ it { expect(@link_to_file).to have_key(:url) }
+ it { expect(@link_to_file).to have_key(:is_image) }
+ it { expect(@link_to_file[:is_image]).to be false }
+ it { expect(@link_to_file[:url]).to match('doc_sample.txt') }
+ it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') }
end
end
end
diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb
new file mode 100644
index 00000000000..ce7d1455975
--- /dev/null
+++ b/spec/services/repair_ldap_blocked_user_service_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe RepairLdapBlockedUserService, services: true do
+ let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
+ let(:identity) { user.ldap_identity }
+ subject(:service) { RepairLdapBlockedUserService.new(user) }
+
+ describe '#execute' do
+ it 'change to normal block after destroying last ldap identity' do
+ identity.destroy
+ service.execute
+
+ expect(user.reload).not_to be_ldap_blocked
+ end
+
+ it 'change to normal block after changing last ldap identity to another provider' do
+ identity.update_attribute(:provider, 'twitter')
+ service.execute
+
+ expect(user.reload).not_to be_ldap_blocked
+ end
+ end
+end
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index febc78d2784..fef211ded50 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -9,37 +9,54 @@ describe SystemHooksService, services: true do
let(:group_member) { create(:group_member) }
context 'event data' do
- it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) }
- it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
- it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
- it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) }
+ it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
+ it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) }
+ it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) }
+ it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
+ it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
it { expect(event_data(key, :create)).to include(:username, :key, :id) }
it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
it do
+ project.old_path_with_namespace = 'renamed_from_path'
+ expect(event_data(project, :rename)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+ it do
+ project.old_path_with_namespace = 'transfered_from_path'
+ expect(event_data(project, :transfer)).to include(
+ :event_name, :name, :created_at, :updated_at, :path, :project_id,
+ :owner_name, :owner_email, :project_visibility,
+ :old_path_with_namespace
+ )
+ end
+
+ it do
expect(event_data(group, :create)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group, :destroy)).to include(
- :event_name, :name, :created_at, :path, :group_id, :owner_name,
- :owner_email
+ :event_name, :name, :created_at, :updated_at, :path, :group_id,
+ :owner_name, :owner_email
)
end
it do
expect(event_data(group_member, :create)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
it do
expect(event_data(group_member, :destroy)).to include(
- :event_name, :created_at, :group_name, :group_path, :group_id, :user_id,
- :user_name, :user_email, :group_access
+ :event_name, :created_at, :updated_at, :group_name, :group_path,
+ :group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
end
@@ -49,6 +66,8 @@ describe SystemHooksService, services: true do
it { expect(event_name(user, :destroy)).to eq "user_destroy" }
it { expect(event_name(project, :create)).to eq "project_create" }
it { expect(event_name(project, :destroy)).to eq "project_destroy" }
+ it { expect(event_name(project, :rename)).to eq "project_rename" }
+ it { expect(event_name(project, :transfer)).to eq "project_transfer" }
it { expect(event_name(project_member, :create)).to eq "user_add_to_team" }
it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" }
it { expect(event_name(key, :create)).to eq 'key_create' }
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index c9f828ae2f7..d3364a71022 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -171,7 +171,7 @@ describe SystemNoteService, services: true do
context 'when milestone added' do
it 'sets the note text' do
- expect(subject.note).to eq "Milestone changed to #{milestone.title}"
+ expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}"
end
end
diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb
new file mode 100644
index 00000000000..352a6eeec79
--- /dev/null
+++ b/spec/support/api/pagination_shared_examples.rb
@@ -0,0 +1,20 @@
+# Specs for paginated resources.
+#
+# Requires an API request:
+# let(:request) { get api("/projects/#{project.id}/repository/branches", user) }
+shared_examples 'a paginated resources' do
+ before do
+ # Fires the request
+ request
+ end
+
+ it 'has pagination headers' do
+ expect(response.headers).to include('X-Total')
+ expect(response.headers).to include('X-Total-Pages')
+ expect(response.headers).to include('X-Per-Page')
+ expect(response.headers).to include('X-Page')
+ expect(response.headers).to include('X-Next-Page')
+ expect(response.headers).to include('X-Prev-Page')
+ expect(response.headers).to include('Link')
+ end
+end
diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb
index d6d3062a197..73c6792b65f 100644
--- a/spec/support/markdown_feature.rb
+++ b/spec/support/markdown_feature.rb
@@ -28,6 +28,10 @@ class MarkdownFeature
end
end
+ def project_wiki
+ @project_wiki ||= ProjectWiki.new(project, user)
+ end
+
def issue
@issue ||= create(:issue, project: project)
end
@@ -59,6 +63,10 @@ class MarkdownFeature
@label ||= create(:label, name: 'awaiting feedback', project: project)
end
+ def milestone
+ @milestone ||= create(:milestone, project: project)
+ end
+
# Cross-references -----------------------------------------------------------
def xproject
@@ -93,6 +101,10 @@ class MarkdownFeature
end
end
+ def xmilestone
+ @xmilestone ||= create(:milestone, project: xproject)
+ end
+
def urls
Gitlab::Application.routes.url_helpers
end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 7eadcd58c1f..1d52489e804 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -66,6 +66,24 @@ module MarkdownMatchers
end
end
+ # GollumTagsFilter
+ matcher :parse_gollum_tags do
+ def have_image(src)
+ have_css("img[src$='#{src}']")
+ end
+
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_link('linked-resource', href: 'linked-resource')
+ expect(actual).to have_link('link-text', href: 'linked-resource')
+ expect(actual).to have_link('http://example.com', href: 'http://example.com')
+ expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf')
+ expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg')
+ expect(actual).to have_image('http://example.com/images/example.jpg')
+ end
+ end
+
# UserReferenceFilter
matcher :reference_users do
set_default_markdown_messages
@@ -130,6 +148,15 @@ module MarkdownMatchers
end
end
+ # MilestoneReferenceFilter
+ matcher :reference_milestones do
+ set_default_markdown_messages
+
+ match do |actual|
+ expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3)
+ end
+ end
+
# TaskListFilter
matcher :parse_task_lists do
set_default_markdown_messages
diff --git a/spec/workers/metrics_worker_spec.rb b/spec/workers/metrics_worker_spec.rb
deleted file mode 100644
index 18260ea0c24..00000000000
--- a/spec/workers/metrics_worker_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-require 'spec_helper'
-
-describe MetricsWorker do
- let(:worker) { described_class.new }
-
- describe '#perform' 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)
-
- worker.perform([{ 'series' => 'kittens', 'tags' => {} }])
- end
- end
-
- describe '#prepare_metrics' do
- it 'returns a Hash with the keys as Symbols' do
- metrics = worker.prepare_metrics([{ 'values' => {}, 'tags' => {} }])
-
- expect(metrics).to eq([{ values: {}, tags: {} }])
- end
-
- it 'escapes tag values' do
- metrics = worker.prepare_metrics([
- { 'values' => {}, 'tags' => { 'foo' => 'bar=' } }
- ])
-
- expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }])
- end
-
- it 'drops empty tags' do
- metrics = worker.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(worker.escape_value('foo=')).to eq('foo\\=')
- end
-
- it 'casts values to Strings' do
- expect(worker.escape_value(10)).to eq('10')
- end
- end
-end
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
new file mode 100755
index 00000000000..cfa49e72c50
--- /dev/null
+++ b/vendor/assets/javascripts/autosize.js
@@ -0,0 +1,243 @@
+/*!
+ Autosize 3.0.14
+ license: MIT
+ http://www.jacklmoore.com/autosize
+*/
+(function (global, factory) {
+ if (typeof define === 'function' && define.amd) {
+ define(['exports', 'module'], factory);
+ } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
+ factory(exports, module);
+ } else {
+ var mod = {
+ exports: {}
+ };
+ factory(mod.exports, mod);
+ global.autosize = mod.exports;
+ }
+})(this, function (exports, module) {
+ 'use strict';
+
+ var set = typeof Set === 'function' ? new Set() : (function () {
+ var list = [];
+
+ return {
+ has: function has(key) {
+ return Boolean(list.indexOf(key) > -1);
+ },
+ add: function add(key) {
+ list.push(key);
+ },
+ 'delete': function _delete(key) {
+ list.splice(list.indexOf(key), 1);
+ } };
+ })();
+
+ function assign(ta) {
+ var _ref = arguments[1] === undefined ? {} : arguments[1];
+
+ var _ref$setOverflowX = _ref.setOverflowX;
+ var setOverflowX = _ref$setOverflowX === undefined ? true : _ref$setOverflowX;
+ var _ref$setOverflowY = _ref.setOverflowY;
+ var setOverflowY = _ref$setOverflowY === undefined ? true : _ref$setOverflowY;
+
+ if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || set.has(ta)) return;
+
+ var heightOffset = null;
+ var overflowY = null;
+ var clientWidth = ta.clientWidth;
+
+ function init() {
+ var style = window.getComputedStyle(ta, null);
+
+ overflowY = style.overflowY;
+
+ if (style.resize === 'vertical') {
+ ta.style.resize = 'none';
+ } else if (style.resize === 'both') {
+ ta.style.resize = 'horizontal';
+ }
+
+ if (style.boxSizing === 'content-box') {
+ heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
+ } else {
+ heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
+ }
+ // Fix when a textarea is not on document body and heightOffset is Not a Number
+ if (isNaN(heightOffset)) {
+ heightOffset = 0;
+ }
+
+ update();
+ }
+
+ function changeOverflow(value) {
+ {
+ // Chrome/Safari-specific fix:
+ // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
+ // made available by removing the scrollbar. The following forces the necessary text reflow.
+ var width = ta.style.width;
+ ta.style.width = '0px';
+ // Force reflow:
+ /* jshint ignore:start */
+ ta.offsetWidth;
+ /* jshint ignore:end */
+ ta.style.width = width;
+ }
+
+ overflowY = value;
+
+ if (setOverflowY) {
+ ta.style.overflowY = value;
+ }
+
+ resize();
+ }
+
+ function resize() {
+ var htmlTop = window.pageYOffset;
+ var bodyTop = document.body.scrollTop;
+ var originalHeight = ta.style.height;
+
+ ta.style.height = 'auto';
+
+ var endHeight = ta.scrollHeight + heightOffset;
+
+ if (ta.scrollHeight === 0) {
+ // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
+ ta.style.height = originalHeight;
+ return;
+ }
+
+ ta.style.height = endHeight + 'px';
+
+ // used to check if an update is actually necessary on window.resize
+ clientWidth = ta.clientWidth;
+
+ // prevents scroll-position jumping
+ document.documentElement.scrollTop = htmlTop;
+ document.body.scrollTop = bodyTop;
+ }
+
+ function update() {
+ var startHeight = ta.style.height;
+
+ resize();
+
+ var style = window.getComputedStyle(ta, null);
+
+ if (style.height !== ta.style.height) {
+ if (overflowY !== 'visible') {
+ changeOverflow('visible');
+ }
+ } else {
+ if (overflowY !== 'hidden') {
+ changeOverflow('hidden');
+ }
+ }
+
+ if (startHeight !== ta.style.height) {
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:resized', true, false);
+ ta.dispatchEvent(evt);
+ }
+ }
+
+ var pageResize = function pageResize() {
+ if (ta.clientWidth !== clientWidth) {
+ update();
+ }
+ };
+
+ var destroy = (function (style) {
+ window.removeEventListener('resize', pageResize, false);
+ ta.removeEventListener('input', update, false);
+ ta.removeEventListener('keyup', update, false);
+ ta.removeEventListener('autosize:destroy', destroy, false);
+ ta.removeEventListener('autosize:update', update, false);
+ set['delete'](ta);
+
+ Object.keys(style).forEach(function (key) {
+ ta.style[key] = style[key];
+ });
+ }).bind(ta, {
+ height: ta.style.height,
+ resize: ta.style.resize,
+ overflowY: ta.style.overflowY,
+ overflowX: ta.style.overflowX,
+ wordWrap: ta.style.wordWrap });
+
+ ta.addEventListener('autosize:destroy', destroy, false);
+
+ // IE9 does not fire onpropertychange or oninput for deletions,
+ // so binding to onkeyup to catch most of those events.
+ // There is no way that I know of to detect something like 'cut' in IE9.
+ if ('onpropertychange' in ta && 'oninput' in ta) {
+ ta.addEventListener('keyup', update, false);
+ }
+
+ window.addEventListener('resize', pageResize, false);
+ ta.addEventListener('input', update, false);
+ ta.addEventListener('autosize:update', update, false);
+ set.add(ta);
+
+ if (setOverflowX) {
+ ta.style.overflowX = 'hidden';
+ ta.style.wordWrap = 'break-word';
+ }
+
+ init();
+ }
+
+ function destroy(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:destroy', true, false);
+ ta.dispatchEvent(evt);
+ }
+
+ function update(ta) {
+ if (!(ta && ta.nodeName && ta.nodeName === 'TEXTAREA')) return;
+ var evt = document.createEvent('Event');
+ evt.initEvent('autosize:update', true, false);
+ ta.dispatchEvent(evt);
+ }
+
+ var autosize = null;
+
+ // Do nothing in Node.js environment and IE8 (or lower)
+ if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
+ autosize = function (el) {
+ return el;
+ };
+ autosize.destroy = function (el) {
+ return el;
+ };
+ autosize.update = function (el) {
+ return el;
+ };
+ } else {
+ autosize = function (el, options) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], function (x) {
+ return assign(x, options);
+ });
+ }
+ return el;
+ };
+ autosize.destroy = function (el) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], destroy);
+ }
+ return el;
+ };
+ autosize.update = function (el) {
+ if (el) {
+ Array.prototype.forEach.call(el.length ? el : [el], update);
+ }
+ return el;
+ };
+ }
+
+ module.exports = autosize;
+}); \ No newline at end of file
diff --git a/vendor/assets/javascripts/fuzzaldrin-plus.min.js b/vendor/assets/javascripts/fuzzaldrin-plus.min.js
new file mode 100644
index 00000000000..3f25c2d8373
--- /dev/null
+++ b/vendor/assets/javascripts/fuzzaldrin-plus.min.js
@@ -0,0 +1 @@
+(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){(function(){var PathSeparator,legacy_scorer,pluckCandidates,scorer,sortCandidates;scorer=require('./scorer');legacy_scorer=require('./legacy');pluckCandidates=function(a){return a.candidate};sortCandidates=function(a,b){return b.score-a.score};PathSeparator=require('path').sep;module.exports=function(candidates,query,arg){var allowErrors,bAllowErrors,bKey,candidate,coreQuery,i,j,key,legacy,len,len1,maxInners,maxResults,prepQuery,queryHasSlashes,ref,score,scoredCandidates,spotLeft,string;ref=arg!=null?arg:{},key=ref.key,maxResults=ref.maxResults,maxInners=ref.maxInners,allowErrors=ref.allowErrors,legacy=ref.legacy;scoredCandidates=[];spotLeft=(maxInners!=null)&&maxInners>0?maxInners:candidates.length;bAllowErrors=!!allowErrors;bKey=key!=null;prepQuery=scorer.prepQuery(query);if(!legacy){for(i=0,len=candidates.length;i<len;i++){candidate=candidates[i];string=bKey?candidate[key]:candidate;if(!string){continue}score=scorer.score(string,query,prepQuery,bAllowErrors);if(score>0){scoredCandidates.push({candidate:candidate,score:score});if(!--spotLeft){break}}}}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;for(j=0,len1=candidates.length;j<len1;j++){candidate=candidates[j];string=key!=null?candidate[key]:candidate;if(!string){continue}score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}if(score>0){scoredCandidates.push({candidate:candidate,score:score})}}}scoredCandidates.sort(sortCandidates);candidates=scoredCandidates.map(pluckCandidates);if(maxResults!=null){candidates=candidates.slice(0,maxResults)}return candidates}}).call(this)},{"./legacy":4,"./scorer":6,"path":7}],2:[function(require,module,exports){(function(){var PathSeparator,filter,legacy_scorer,matcher,prepQueryCache,scorer;scorer=require('./scorer');legacy_scorer=require('./legacy');filter=require('./filter');matcher=require('./matcher');PathSeparator=require('path').sep;prepQueryCache=null;module.exports={filter:function(candidates,query,options){if(!((query!=null?query.length:void 0)&&(candidates!=null?candidates.length:void 0))){return[]}return filter(candidates,query,options)},prepQuery:function(query){return scorer.prepQuery(query)},score:function(string,query,prepQuery,arg){var allowErrors,coreQuery,legacy,queryHasSlashes,ref,score;ref=arg!=null?arg:{},allowErrors=ref.allowErrors,legacy=ref.legacy;if(!((string!=null?string.length:void 0)&&(query!=null?query.length:void 0))){return 0}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!legacy){score=scorer.score(string,query,prepQuery,!!allowErrors)}else{queryHasSlashes=prepQuery.depth>0;coreQuery=prepQuery.core;score=legacy_scorer.score(string,coreQuery,queryHasSlashes);if(!queryHasSlashes){score=legacy_scorer.basenameScore(string,coreQuery,score)}}return score},match:function(string,query,prepQuery,arg){var allowErrors,baseMatches,i,matches,query_lw,ref,results,string_lw;allowErrors=(arg!=null?arg:{}).allowErrors;if(!string){return[]}if(!query){return[]}if(string===query){return(function(){results=[];for(var i=0,ref=string.length;0<=ref?i<ref:i>ref;0<=ref?i++:i--){results.push(i)}return results}).apply(this)}if(prepQuery==null){prepQuery=prepQueryCache&&prepQueryCache.query===query?prepQueryCache:(prepQueryCache=scorer.prepQuery(query))}if(!(allowErrors||scorer.isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return[]}string_lw=string.toLowerCase();query_lw=prepQuery.query_lw;matches=matcher.match(string,string_lw,prepQuery);if(matches.length===0){return matches}if(string.indexOf(PathSeparator)>-1){baseMatches=matcher.basenameMatch(string,string_lw,prepQuery);matches=matcher.mergeMatches(matches,baseMatches)}return matches}}}).call(this)},{"./filter":1,"./legacy":4,"./matcher":5,"./scorer":6,"path":7}],3:[function(require,module,exports){fuzzaldrinPlus=require('./fuzzaldrin')},{"./fuzzaldrin":2}],4:[function(require,module,exports){(function(){var PathSeparator,queryIsLastPathSegment;PathSeparator=require('path').sep;exports.basenameScore=function(string,query,score){var base,depth,index,lastCharacter,segmentCount,slashCount;index=string.length-1;while(string[index]===PathSeparator){index--}slashCount=0;lastCharacter=index;base=null;while(index>=0){if(string[index]===PathSeparator){slashCount++;if(base==null){base=string.substring(index+1,lastCharacter+1)}}else if(index===0){if(lastCharacter<string.length-1){if(base==null){base=string.substring(0,lastCharacter+1)}}else{if(base==null){base=string}}}index--}if(base===string){score*=2}else if(base){score+=exports.score(base,query)}segmentCount=slashCount+1;depth=Math.max(1,10-segmentCount);score*=depth*0.01;return score};exports.score=function(string,query){var character,characterScore,indexInQuery,indexInString,lowerCaseIndex,minIndex,queryLength,queryScore,ref,stringLength,totalCharacterScore,upperCaseIndex;if(string===query){return 1}if(queryIsLastPathSegment(string,query)){return 1}totalCharacterScore=0;queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;while(indexInQuery<queryLength){character=query[indexInQuery++];lowerCaseIndex=string.indexOf(character.toLowerCase());upperCaseIndex=string.indexOf(character.toUpperCase());minIndex=Math.min(lowerCaseIndex,upperCaseIndex);if(minIndex===-1){minIndex=Math.max(lowerCaseIndex,upperCaseIndex)}indexInString=minIndex;if(indexInString===-1){return 0}characterScore=0.1;if(string[indexInString]===character){characterScore+=0.1}if(indexInString===0||string[indexInString-1]===PathSeparator){characterScore+=0.8}else if((ref=string[indexInString-1])==='-'||ref==='_'||ref===' '){characterScore+=0.7}string=string.substring(indexInString+1,stringLength);totalCharacterScore+=characterScore}queryScore=totalCharacterScore/queryLength;return((queryScore*(queryLength/stringLength))+queryScore)/2};queryIsLastPathSegment=function(string,query){if(string[string.length-query.length-1]===PathSeparator){return string.lastIndexOf(query)===string.length-query.length}};exports.match=function(string,query,stringOffset){var character,i,indexInQuery,indexInString,lowerCaseIndex,matches,minIndex,queryLength,ref,results,stringLength,upperCaseIndex;if(stringOffset==null){stringOffset=0}if(string===query){return(function(){results=[];for(var i=stringOffset,ref=stringOffset+string.length;stringOffset<=ref?i<ref:i>ref;stringOffset<=ref?i++:i--){results.push(i)}return results}).apply(this)}queryLength=query.length;stringLength=string.length;indexInQuery=0;indexInString=0;matches=[];while(indexInQuery<queryLength){character=query[indexInQuery++];lowerCaseIndex=string.indexOf(character.toLowerCase());upperCaseIndex=string.indexOf(character.toUpperCase());minIndex=Math.min(lowerCaseIndex,upperCaseIndex);if(minIndex===-1){minIndex=Math.max(lowerCaseIndex,upperCaseIndex)}indexInString=minIndex;if(indexInString===-1){return[]}matches.push(stringOffset+indexInString);stringOffset+=indexInString+1;string=string.substring(indexInString+1,stringLength)}return matches}}).call(this)},{"path":7}],5:[function(require,module,exports){(function(){var PathSeparator,scorer;PathSeparator=require('path').sep;scorer=require('./scorer');exports.basenameMatch=function(subject,subject_lw,prepQuery){var basePos,depth,end;end=subject.length-1;while(subject[end]===PathSeparator){end--}basePos=subject.lastIndexOf(PathSeparator,end);if(basePos===-1){return[]}depth=prepQuery.depth;while(depth-->0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return[]}}basePos++;end++;return exports.match(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery,basePos)};exports.mergeMatches=function(a,b){var ai,bj,i,j,m,n,out;m=a.length;n=b.length;if(n===0){return a.slice()}if(m===0){return b.slice()}i=-1;j=0;bj=b[j];out=[];while(++i<m){ai=a[i];while(bj<=ai&&++j<n){if(bj<ai){out.push(bj)}bj=b[j]}out.push(ai)}while(j<n){out.push(b[j++])}return out};exports.match=function(subject,subject_lw,prepQuery,offset){var DIAGONAL,LEFT,STOP,UP,acro_score,align,backtrack,csc_diag,csc_row,csc_score,i,j,m,matches,move,n,pos,query,query_lw,score,score_diag,score_row,score_up,si_lw,start,trace;if(offset==null){offset=0}query=prepQuery.query;query_lw=prepQuery.query_lw;m=subject.length;n=query.length;acro_score=scorer.scoreAcronyms(subject,subject_lw,query,query_lw).score;score_row=new Array(n);csc_row=new Array(n);STOP=0;UP=1;LEFT=2;DIAGONAL=3;trace=new Array(m*n);pos=-1;j=-1;while(++j<n){score_row[j]=0;csc_row[j]=0}i=-1;while(++i<m){score=0;score_up=0;csc_diag=0;si_lw=subject_lw[i];j=-1;while(++j<n){csc_score=0;align=0;score_diag=score_up;if(query_lw[j]===si_lw){start=scorer.isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scorer.scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scorer.scoreCharacter(i,j,start,acro_score,csc_score)}score_up=score_row[j];csc_diag=csc_row[j];if(score>score_up){move=LEFT}else{score=score_up;move=UP}if(align>score){score=align;move=DIAGONAL}else{csc_score=0}score_row[j]=score;csc_row[j]=csc_score;trace[++pos]=score>0?move:STOP}}i=m-1;j=n-1;pos=i*n+j;backtrack=true;matches=[];while(backtrack&&i>=0&&j>=0){switch(trace[pos]){case UP:i--;pos-=n;break;case LEFT:j--;pos--;break;case DIAGONAL:matches.push(i+offset);j--;i--;pos-=n+1;break;default:backtrack=false}}matches.reverse();return matches}}).call(this)},{"./scorer":6,"path":7}],6:[function(require,module,exports){(function(){var AcronymResult,PathSeparator,Query,basenameScore,coreChars,countDir,doScore,emptyAcronymResult,file_coeff,isMatch,isSeparator,isWordEnd,isWordStart,miss_coeff,opt_char_re,pos_bonus,scoreAcronyms,scoreCharacter,scoreConsecutives,scoreExact,scoreExactMatch,scorePattern,scorePosition,scoreSize,tau_depth,tau_size,truncatedUpperCase,wm;PathSeparator=require('path').sep;wm=150;pos_bonus=20;tau_depth=13;tau_size=85;file_coeff=1.2;miss_coeff=0.75;opt_char_re=/[ _\-:\/\\]/g;exports.coreChars=coreChars=function(query){return query.replace(opt_char_re,'')};exports.score=function(string,query,prepQuery,allowErrors){var score,string_lw;if(prepQuery==null){prepQuery=new Query(query)}if(allowErrors==null){allowErrors=false}if(!(allowErrors||isMatch(string,prepQuery.core_lw,prepQuery.core_up))){return 0}string_lw=string.toLowerCase();score=doScore(string,string_lw,prepQuery);return Math.ceil(basenameScore(string,string_lw,prepQuery,score))};Query=(function(){function Query(query){if(!(query!=null?query.length:void 0)){return null}this.query=query;this.query_lw=query.toLowerCase();this.core=coreChars(query);this.core_lw=this.core.toLowerCase();this.core_up=truncatedUpperCase(this.core);this.depth=countDir(query,query.length)}return Query})();exports.prepQuery=function(query){return new Query(query)};exports.isMatch=isMatch=function(subject,query_lw,query_up){var i,j,m,n,qj_lw,qj_up,si;m=subject.length;n=query_lw.length;if(!m||!n||n>m){return false}i=-1;j=-1;while(++j<n){qj_lw=query_lw[j];qj_up=query_up[j];while(++i<m){si=subject[i];if(si===qj_lw||si===qj_up){break}}if(i===m){return false}}return true};doScore=function(subject,subject_lw,prepQuery){var acro,acro_score,align,csc_diag,csc_row,csc_score,i,j,m,miss_budget,miss_left,mm,n,pos,query,query_lw,record_miss,score,score_diag,score_row,score_up,si_lw,start,sz;query=prepQuery.query;query_lw=prepQuery.query_lw;m=subject.length;n=query.length;acro=scoreAcronyms(subject,subject_lw,query,query_lw);acro_score=acro.score;if(acro.count===n){return scoreExact(n,m,acro_score,acro.pos)}pos=subject_lw.indexOf(query_lw);if(pos>-1){return scoreExactMatch(subject,subject_lw,query,query_lw,pos,n,m)}score_row=new Array(n);csc_row=new Array(n);sz=scoreSize(n,m);miss_budget=Math.ceil(miss_coeff*n)+5;miss_left=miss_budget;j=-1;while(++j<n){score_row[j]=0;csc_row[j]=0}i=subject_lw.indexOf(query_lw[0]);if(i>-1){i--}mm=subject_lw.lastIndexOf(query_lw[n-1],m);if(mm>i){m=mm+1}while(++i<m){score=0;score_diag=0;csc_diag=0;si_lw=subject_lw[i];record_miss=true;j=-1;while(++j<n){score_up=score_row[j];if(score_up>score){score=score_up}csc_score=0;if(query_lw[j]===si_lw){start=isWordStart(i,subject,subject_lw);csc_score=csc_diag>0?csc_diag:scoreConsecutives(subject,subject_lw,query,query_lw,i,j,start);align=score_diag+scoreCharacter(i,j,start,acro_score,csc_score);if(align>score){score=align;miss_left=miss_budget}else{if(record_miss&&--miss_left<=0){return score_row[n-1]*sz}record_miss=false}}score_diag=score_up;csc_diag=csc_row[j];csc_row[j]=csc_score;score_row[j]=score}}return score*sz};exports.isWordStart=isWordStart=function(pos,subject,subject_lw){var curr_s,prev_s;if(pos===0){return true}curr_s=subject[pos];prev_s=subject[pos-1];return isSeparator(curr_s)||isSeparator(prev_s)||(curr_s!==subject_lw[pos]&&prev_s===subject_lw[pos-1])};exports.isWordEnd=isWordEnd=function(pos,subject,subject_lw,len){var curr_s,next_s;if(pos===len-1){return true}curr_s=subject[pos];next_s=subject[pos+1];return isSeparator(curr_s)||isSeparator(next_s)||(curr_s===subject_lw[pos]&&next_s!==subject_lw[pos+1])};isSeparator=function(c){return c===' '||c==='.'||c==='-'||c==='_'||c==='/'||c==='\\'};scorePosition=function(pos){var sc;if(pos<pos_bonus){sc=pos_bonus-pos;return 100+sc*sc}else{return Math.max(100+pos_bonus-pos,0)}};scoreSize=function(n,m){return tau_size/(tau_size+Math.abs(m-n))};scoreExact=function(n,m,quality,pos){return 2*n*(wm*quality+scorePosition(pos))*scoreSize(n,m)};exports.scorePattern=scorePattern=function(count,len,sameCase,start,end){var bonus,sz;sz=count;bonus=6;if(sameCase===count){bonus+=2}if(start){bonus+=3}if(end){bonus+=1}if(count===len){if(start){if(sameCase===len){sz+=2}else{sz+=1}}if(end){bonus+=1}}return sameCase+sz*(sz+bonus)};exports.scoreCharacter=scoreCharacter=function(i,j,start,acro_score,csc_score){var posBonus;posBonus=scorePosition(i);if(start){return posBonus+wm*((acro_score>csc_score?acro_score:csc_score)+10)}return posBonus+wm*csc_score};exports.scoreConsecutives=scoreConsecutives=function(subject,subject_lw,query,query_lw,i,j,start){var k,m,mi,n,nj,sameCase,startPos,sz;m=subject.length;n=query.length;mi=m-i;nj=n-j;k=mi<nj?mi:nj;startPos=i;sameCase=0;sz=0;if(query[j]===subject[i]){sameCase++}while(++sz<k&&query_lw[++j]===subject_lw[++i]){if(query[j]===subject[i]){sameCase++}}if(sz===1){return 1+2*sameCase}return scorePattern(sz,n,sameCase,start,isWordEnd(i,subject,subject_lw,m))};exports.scoreExactMatch=scoreExactMatch=function(subject,subject_lw,query,query_lw,pos,n,m){var end,i,pos2,sameCase,start;start=isWordStart(pos,subject,subject_lw);if(!start){pos2=subject_lw.indexOf(query_lw,pos+1);if(pos2>-1){start=isWordStart(pos2,subject,subject_lw);if(start){pos=pos2}}}i=-1;sameCase=0;while(++i<n){if(query[pos+i]===subject[i]){sameCase++}}end=isWordEnd(pos+n-1,subject,subject_lw,m);return scoreExact(n,m,scorePattern(n,n,sameCase,start,end),pos)};AcronymResult=(function(){function AcronymResult(score1,pos1,count1){this.score=score1;this.pos=pos1;this.count=count1}return AcronymResult})();emptyAcronymResult=new AcronymResult(0,0.1,0);exports.scoreAcronyms=scoreAcronyms=function(subject,subject_lw,query,query_lw){var count,i,j,m,n,pos,qj_lw,sameCase,score;m=subject.length;n=query.length;if(!(m>1&&n>1)){return emptyAcronymResult}count=0;pos=0;sameCase=0;i=-1;j=-1;while(++j<n){qj_lw=query_lw[j];while(++i<m){if(qj_lw===subject_lw[i]&&isWordStart(i,subject,subject_lw)){if(query[j]===subject[i]){sameCase++}pos+=i;count++;break}}if(i===m){break}}if(count<2){return emptyAcronymResult}score=scorePattern(count,n,sameCase,true,false);return new AcronymResult(score,pos/count,count)};basenameScore=function(subject,subject_lw,prepQuery,fullPathScore){var alpha,basePathScore,basePos,depth,end;if(fullPathScore===0){return 0}end=subject.length-1;while(subject[end]===PathSeparator){end--}basePos=subject.lastIndexOf(PathSeparator,end);if(basePos===-1){return fullPathScore}depth=prepQuery.depth;while(depth-->0){basePos=subject.lastIndexOf(PathSeparator,basePos-1);if(basePos===-1){return fullPathScore}}basePos++;end++;basePathScore=doScore(subject.slice(basePos,end),subject_lw.slice(basePos,end),prepQuery);alpha=0.5*tau_depth/(tau_depth+countDir(subject,end+1));return alpha*basePathScore+(1-alpha)*fullPathScore*scoreSize(0,file_coeff*(end-basePos))};exports.countDir=countDir=function(path,end){var count,i;if(end<1){return 0}count=0;i=-1;while(++i<end&&path[i]===PathSeparator){continue}while(++i<end){if(path[i]===PathSeparator){count++;while(++i<end&&path[i]===PathSeparator){continue}}}return count};truncatedUpperCase=function(str){var char,l,len1,upper;upper="";for(l=0,len1=str.length;l<len1;l++){char=str[l];upper+=char.toUpperCase()[0]}return upper}}).call(this)},{"path":7}],7:[function(require,module,exports){(function(process){function normalizeArray(parts,allowAboveRoot){var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==='.'){parts.splice(i,1)}else if(last==='..'){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up--;up){parts.unshift('..')}}return parts}var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;var splitPath=function(filename){return splitPathRe.exec(filename).slice(1)};exports.resolve=function(){var resolvedPath='',resolvedAbsolute=false;for(var i=arguments.length-1;i>=-1&&!resolvedAbsolute;i--){var path=(i>=0)?arguments[i]:process.cwd();if(typeof path!=='string'){throw new TypeError('Arguments to path.resolve must be strings');}else if(!path){continue}resolvedPath=path+'/'+resolvedPath;resolvedAbsolute=path.charAt(0)==='/'}resolvedPath=normalizeArray(filter(resolvedPath.split('/'),function(p){return!!p}),!resolvedAbsolute).join('/');return((resolvedAbsolute?'/':'')+resolvedPath)||'.'};exports.normalize=function(path){var isAbsolute=exports.isAbsolute(path),trailingSlash=substr(path,-1)==='/';path=normalizeArray(filter(path.split('/'),function(p){return!!p}),!isAbsolute).join('/');if(!path&&!isAbsolute){path='.'}if(path&&trailingSlash){path+='/'}return(isAbsolute?'/':'')+path};exports.isAbsolute=function(path){return path.charAt(0)==='/'};exports.join=function(){var paths=Array.prototype.slice.call(arguments,0);return exports.normalize(filter(paths,function(p,index){if(typeof p!=='string'){throw new TypeError('Arguments to path.join must be strings');}return p}).join('/'))};exports.relative=function(from,to){from=exports.resolve(from).substr(1);to=exports.resolve(to).substr(1);function trim(arr){var start=0;for(;start<arr.length;start++){if(arr[start]!=='')break}var end=arr.length-1;for(;end>=0;end--){if(arr[end]!=='')break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split('/'));var toParts=trim(to.split('/'));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i<length;i++){if(fromParts[i]!==toParts[i]){samePartsLength=i;break}}var outputParts=[];for(var i=samePartsLength;i<fromParts.length;i++){outputParts.push('..')}outputParts=outputParts.concat(toParts.slice(samePartsLength));return outputParts.join('/')};exports.sep='/';exports.delimiter=':';exports.dirname=function(path){var result=splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return'.'}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir};exports.basename=function(path,ext){var f=splitPath(path)[2];if(ext&&f.substr(-1*ext.length)===ext){f=f.substr(0,f.length-ext.length)}return f};exports.extname=function(path){return splitPath(path)[3]};function filter(xs,f){if(xs.filter)return xs.filter(f);var res=[];for(var i=0;i<xs.length;i++){if(f(xs[i],i,xs))res.push(xs[i])}return res}var substr='ab'.substr(-1)==='b'?function(str,start,len){return str.substr(start,len)}:function(str,start,len){if(start<0)start=str.length+start;return str.substr(start,len)}}).call(this,require('_process'))},{"_process":8}],8:[function(require,module,exports){var process=module.exports={};var queue=[];var draining=false;var currentQueue;var queueIndex=-1;function cleanUpNextTick(){draining=false;if(currentQueue.length){queue=currentQueue.concat(queue)}else{queueIndex=-1}if(queue.length){drainQueue()}}function drainQueue(){if(draining){return}var timeout=setTimeout(cleanUpNextTick);draining=true;var len=queue.length;while(len){currentQueue=queue;queue=[];while(++queueIndex<len){if(currentQueue){currentQueue[queueIndex].run()}}queueIndex=-1;len=queue.length}currentQueue=null;draining=false;clearTimeout(timeout)}process.nextTick=function(fun){var args=new Array(arguments.length-1);if(arguments.length>1){for(var i=1;i<arguments.length;i++){args[i-1]=arguments[i]}}queue.push(new Item(fun,args));if(queue.length===1&&!draining){setTimeout(drainQueue,0)}};function Item(fun,array){this.fun=fun;this.array=array}Item.prototype.run=function(){this.fun.apply(null,this.array)};process.title='browser';process.browser=true;process.env={};process.argv=[];process.version='';process.versions={};function noop(){}process.on=noop;process.addListener=noop;process.once=noop;process.off=noop;process.removeListener=noop;process.removeAllListeners=noop;process.emit=noop;process.binding=function(name){throw new Error('process.binding is not supported');};process.cwd=function(){return'/'};process.chdir=function(dir){throw new Error('process.chdir is not supported');};process.umask=function(){return 0}},{}]},{},[3]);
diff --git a/vendor/assets/javascripts/jquery.blockUI.js b/vendor/assets/javascripts/jquery.blockUI.js
deleted file mode 100644
index c8702d79b65..00000000000
--- a/vendor/assets/javascripts/jquery.blockUI.js
+++ /dev/null
@@ -1,590 +0,0 @@
-/*!
- * jQuery blockUI plugin
- * Version 2.60.0-2013.04.05
- * @requires jQuery v1.7 or later
- *
- * Examples at: http://malsup.com/jquery/block/
- * Copyright (c) 2007-2013 M. Alsup
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- * Thanks to Amir-Hossein Sobhi for some excellent contributions!
- */
-
-;(function() {
-/*jshint eqeqeq:false curly:false latedef:false */
-"use strict";
-
- function setup($) {
- $.fn._fadeIn = $.fn.fadeIn;
-
- var noOp = $.noop || function() {};
-
- // this bit is to ensure we don't call setExpression when we shouldn't (with extra muscle to handle
- // retarded userAgent strings on Vista)
- var msie = /MSIE/.test(navigator.userAgent);
- var ie6 = /MSIE 6.0/.test(navigator.userAgent) && ! /MSIE 8.0/.test(navigator.userAgent);
- var mode = document.documentMode || 0;
- var setExpr = $.isFunction( document.createElement('div').style.setExpression );
-
- // global $ methods for blocking/unblocking the entire page
- $.blockUI = function(opts) { install(window, opts); };
- $.unblockUI = function(opts) { remove(window, opts); };
-
- // convenience method for quick growl-like notifications (http://www.google.com/search?q=growl)
- $.growlUI = function(title, message, timeout, onClose) {
- var $m = $('<div class="growlUI"></div>');
- if (title) $m.append('<h1>'+title+'</h1>');
- if (message) $m.append('<h2>'+message+'</h2>');
- if (timeout === undefined) timeout = 3000;
- $.blockUI({
- message: $m, fadeIn: 700, fadeOut: 1000, centerY: false,
- timeout: timeout, showOverlay: false,
- onUnblock: onClose,
- css: $.blockUI.defaults.growlCSS
- });
- };
-
- // plugin method for blocking element content
- $.fn.block = function(opts) {
- if ( this[0] === window ) {
- $.blockUI( opts );
- return this;
- }
- var fullOpts = $.extend({}, $.blockUI.defaults, opts || {});
- this.each(function() {
- var $el = $(this);
- if (fullOpts.ignoreIfBlocked && $el.data('blockUI.isBlocked'))
- return;
- $el.unblock({ fadeOut: 0 });
- });
-
- return this.each(function() {
- if ($.css(this,'position') == 'static') {
- this.style.position = 'relative';
- $(this).data('blockUI.static', true);
- }
- this.style.zoom = 1; // force 'hasLayout' in ie
- install(this, opts);
- });
- };
-
- // plugin method for unblocking element content
- $.fn.unblock = function(opts) {
- if ( this[0] === window ) {
- $.unblockUI( opts );
- return this;
- }
- return this.each(function() {
- remove(this, opts);
- });
- };
-
- $.blockUI.version = 2.60; // 2nd generation blocking at no extra cost!
-
- // override these in your code to change the default behavior and style
- $.blockUI.defaults = {
- // message displayed when blocking (use null for no message)
- message: '<h1>Please wait...</h1>',
-
- title: null, // title string; only used when theme == true
- draggable: true, // only used when theme == true (requires jquery-ui.js to be loaded)
-
- theme: false, // set to true to use with jQuery UI themes
-
- // styles for the message when blocking; if you wish to disable
- // these and use an external stylesheet then do this in your code:
- // $.blockUI.defaults.css = {};
- css: {
- padding: 0,
- margin: 0,
- width: '30%',
- top: '40%',
- left: '35%',
- textAlign: 'center',
- color: '#000',
- border: '3px solid #aaa',
- backgroundColor:'#fff',
- cursor: 'wait'
- },
-
- // minimal style set used when themes are used
- themedCSS: {
- width: '30%',
- top: '40%',
- left: '35%'
- },
-
- // styles for the overlay
- overlayCSS: {
- backgroundColor: '#000',
- opacity: 0.6,
- cursor: 'wait'
- },
-
- // style to replace wait cursor before unblocking to correct issue
- // of lingering wait cursor
- cursorReset: 'default',
-
- // styles applied when using $.growlUI
- growlCSS: {
- width: '350px',
- top: '10px',
- left: '',
- right: '10px',
- border: 'none',
- padding: '5px',
- opacity: 0.6,
- cursor: 'default',
- color: '#fff',
- backgroundColor: '#000',
- '-webkit-border-radius':'10px',
- '-moz-border-radius': '10px',
- 'border-radius': '10px'
- },
-
- // IE issues: 'about:blank' fails on HTTPS and javascript:false is s-l-o-w
- // (hat tip to Jorge H. N. de Vasconcelos)
- /*jshint scripturl:true */
- iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank',
-
- // force usage of iframe in non-IE browsers (handy for blocking applets)
- forceIframe: false,
-
- // z-index for the blocking overlay
- baseZ: 1000,
-
- // set these to true to have the message automatically centered
- centerX: true, // <-- only effects element blocking (page block controlled via css above)
- centerY: true,
-
- // allow body element to be stetched in ie6; this makes blocking look better
- // on "short" pages. disable if you wish to prevent changes to the body height
- allowBodyStretch: true,
-
- // enable if you want key and mouse events to be disabled for content that is blocked
- bindEvents: true,
-
- // be default blockUI will supress tab navigation from leaving blocking content
- // (if bindEvents is true)
- constrainTabKey: true,
-
- // fadeIn time in millis; set to 0 to disable fadeIn on block
- fadeIn: 200,
-
- // fadeOut time in millis; set to 0 to disable fadeOut on unblock
- fadeOut: 400,
-
- // time in millis to wait before auto-unblocking; set to 0 to disable auto-unblock
- timeout: 0,
-
- // disable if you don't want to show the overlay
- showOverlay: true,
-
- // if true, focus will be placed in the first available input field when
- // page blocking
- focusInput: true,
-
- // elements that can receive focus
- focusableElements: ':input:enabled:visible',
-
- // suppresses the use of overlay styles on FF/Linux (due to performance issues with opacity)
- // no longer needed in 2012
- // applyPlatformOpacityRules: true,
-
- // callback method invoked when fadeIn has completed and blocking message is visible
- onBlock: null,
-
- // callback method invoked when unblocking has completed; the callback is
- // passed the element that has been unblocked (which is the window object for page
- // blocks) and the options that were passed to the unblock call:
- // onUnblock(element, options)
- onUnblock: null,
-
- // callback method invoked when the overlay area is clicked.
- // setting this will turn the cursor to a pointer, otherwise cursor defined in overlayCss will be used.
- onOverlayClick: null,
-
- // don't ask; if you really must know: http://groups.google.com/group/jquery-en/browse_thread/thread/36640a8730503595/2f6a79a77a78e493#2f6a79a77a78e493
- quirksmodeOffsetHack: 4,
-
- // class name of the message block
- blockMsgClass: 'blockMsg',
-
- // if it is already blocked, then ignore it (don't unblock and reblock)
- ignoreIfBlocked: false
- };
-
- // private data and functions follow...
-
- var pageBlock = null;
- var pageBlockEls = [];
-
- function install(el, opts) {
- var css, themedCSS;
- var full = (el == window);
- var msg = (opts && opts.message !== undefined ? opts.message : undefined);
- opts = $.extend({}, $.blockUI.defaults, opts || {});
-
- if (opts.ignoreIfBlocked && $(el).data('blockUI.isBlocked'))
- return;
-
- opts.overlayCSS = $.extend({}, $.blockUI.defaults.overlayCSS, opts.overlayCSS || {});
- css = $.extend({}, $.blockUI.defaults.css, opts.css || {});
- if (opts.onOverlayClick)
- opts.overlayCSS.cursor = 'pointer';
-
- themedCSS = $.extend({}, $.blockUI.defaults.themedCSS, opts.themedCSS || {});
- msg = msg === undefined ? opts.message : msg;
-
- // remove the current block (if there is one)
- if (full && pageBlock)
- remove(window, {fadeOut:0});
-
- // if an existing element is being used as the blocking content then we capture
- // its current place in the DOM (and current display style) so we can restore
- // it when we unblock
- if (msg && typeof msg != 'string' && (msg.parentNode || msg.jquery)) {
- var node = msg.jquery ? msg[0] : msg;
- var data = {};
- $(el).data('blockUI.history', data);
- data.el = node;
- data.parent = node.parentNode;
- data.display = node.style.display;
- data.position = node.style.position;
- if (data.parent)
- data.parent.removeChild(node);
- }
-
- $(el).data('blockUI.onUnblock', opts.onUnblock);
- var z = opts.baseZ;
-
- // blockUI uses 3 layers for blocking, for simplicity they are all used on every platform;
- // layer1 is the iframe layer which is used to supress bleed through of underlying content
- // layer2 is the overlay layer which has opacity and a wait cursor (by default)
- // layer3 is the message content that is displayed while blocking
- var lyr1, lyr2, lyr3, s;
- if (msie || opts.forceIframe)
- lyr1 = $('<iframe class="blockUI" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;position:absolute;width:100%;height:100%;top:0;left:0" src="'+opts.iframeSrc+'"></iframe>');
- else
- lyr1 = $('<div class="blockUI" style="display:none"></div>');
-
- if (opts.theme)
- lyr2 = $('<div class="blockUI blockOverlay ui-widget-overlay" style="z-index:'+ (z++) +';display:none"></div>');
- else
- lyr2 = $('<div class="blockUI blockOverlay" style="z-index:'+ (z++) +';display:none;border:none;margin:0;padding:0;width:100%;height:100%;top:0;left:0"></div>');
-
- if (opts.theme && full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:fixed">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (opts.theme) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement ui-dialog ui-widget ui-corner-all" style="z-index:'+(z+10)+';display:none;position:absolute">';
- if ( opts.title ) {
- s += '<div class="ui-widget-header ui-dialog-titlebar ui-corner-all blockTitle">'+(opts.title || '&nbsp;')+'</div>';
- }
- s += '<div class="ui-widget-content ui-dialog-content"></div>';
- s += '</div>';
- }
- else if (full) {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockPage" style="z-index:'+(z+10)+';display:none;position:fixed"></div>';
- }
- else {
- s = '<div class="blockUI ' + opts.blockMsgClass + ' blockElement" style="z-index:'+(z+10)+';display:none;position:absolute"></div>';
- }
- lyr3 = $(s);
-
- // if we have a message, style it
- if (msg) {
- if (opts.theme) {
- lyr3.css(themedCSS);
- lyr3.addClass('ui-widget-content');
- }
- else
- lyr3.css(css);
- }
-
- // style the overlay
- if (!opts.theme /*&& (!opts.applyPlatformOpacityRules)*/)
- lyr2.css(opts.overlayCSS);
- lyr2.css('position', full ? 'fixed' : 'absolute');
-
- // make iframe layer transparent in IE
- if (msie || opts.forceIframe)
- lyr1.css('opacity',0.0);
-
- //$([lyr1[0],lyr2[0],lyr3[0]]).appendTo(full ? 'body' : el);
- var layers = [lyr1,lyr2,lyr3], $par = full ? $('body') : $(el);
- $.each(layers, function() {
- this.appendTo($par);
- });
-
- if (opts.theme && opts.draggable && $.fn.draggable) {
- lyr3.draggable({
- handle: '.ui-dialog-titlebar',
- cancel: 'li'
- });
- }
-
- // ie7 must use absolute positioning in quirks mode and to account for activex issues (when scrolling)
- var expr = setExpr && (!$.support.boxModel || $('object,embed', full ? null : el).length > 0);
- if (ie6 || expr) {
- // give body 100% height
- if (full && opts.allowBodyStretch && $.support.boxModel)
- $('html,body').css('height','100%');
-
- // fix ie6 issue when blocked element has a border width
- if ((ie6 || !$.support.boxModel) && !full) {
- var t = sz(el,'borderTopWidth'), l = sz(el,'borderLeftWidth');
- var fixT = t ? '(0 - '+t+')' : 0;
- var fixL = l ? '(0 - '+l+')' : 0;
- }
-
- // simulate fixed position
- $.each(layers, function(i,o) {
- var s = o[0].style;
- s.position = 'absolute';
- if (i < 2) {
- if (full)
- s.setExpression('height','Math.max(document.body.scrollHeight, document.body.offsetHeight) - (jQuery.support.boxModel?0:'+opts.quirksmodeOffsetHack+') + "px"');
- else
- s.setExpression('height','this.parentNode.offsetHeight + "px"');
- if (full)
- s.setExpression('width','jQuery.support.boxModel && document.documentElement.clientWidth || document.body.clientWidth + "px"');
- else
- s.setExpression('width','this.parentNode.offsetWidth + "px"');
- if (fixL) s.setExpression('left', fixL);
- if (fixT) s.setExpression('top', fixT);
- }
- else if (opts.centerY) {
- if (full) s.setExpression('top','(document.documentElement.clientHeight || document.body.clientHeight) / 2 - (this.offsetHeight / 2) + (blah = document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + "px"');
- s.marginTop = 0;
- }
- else if (!opts.centerY && full) {
- var top = (opts.css && opts.css.top) ? parseInt(opts.css.top, 10) : 0;
- var expression = '((document.documentElement.scrollTop ? document.documentElement.scrollTop : document.body.scrollTop) + '+top+') + "px"';
- s.setExpression('top',expression);
- }
- });
- }
-
- // show the message
- if (msg) {
- if (opts.theme)
- lyr3.find('.ui-widget-content').append(msg);
- else
- lyr3.append(msg);
- if (msg.jquery || msg.nodeType)
- $(msg).show();
- }
-
- if ((msie || opts.forceIframe) && opts.showOverlay)
- lyr1.show(); // opacity is zero
- if (opts.fadeIn) {
- var cb = opts.onBlock ? opts.onBlock : noOp;
- var cb1 = (opts.showOverlay && !msg) ? cb : noOp;
- var cb2 = msg ? cb : noOp;
- if (opts.showOverlay)
- lyr2._fadeIn(opts.fadeIn, cb1);
- if (msg)
- lyr3._fadeIn(opts.fadeIn, cb2);
- }
- else {
- if (opts.showOverlay)
- lyr2.show();
- if (msg)
- lyr3.show();
- if (opts.onBlock)
- opts.onBlock();
- }
-
- // bind key and mouse events
- bind(1, el, opts);
-
- if (full) {
- pageBlock = lyr3[0];
- pageBlockEls = $(opts.focusableElements,pageBlock);
- if (opts.focusInput)
- setTimeout(focus, 20);
- }
- else
- center(lyr3[0], opts.centerX, opts.centerY);
-
- if (opts.timeout) {
- // auto-unblock
- var to = setTimeout(function() {
- if (full)
- $.unblockUI(opts);
- else
- $(el).unblock(opts);
- }, opts.timeout);
- $(el).data('blockUI.timeout', to);
- }
- }
-
- // remove the block
- function remove(el, opts) {
- var count;
- var full = (el == window);
- var $el = $(el);
- var data = $el.data('blockUI.history');
- var to = $el.data('blockUI.timeout');
- if (to) {
- clearTimeout(to);
- $el.removeData('blockUI.timeout');
- }
- opts = $.extend({}, $.blockUI.defaults, opts || {});
- bind(0, el, opts); // unbind events
-
- if (opts.onUnblock === null) {
- opts.onUnblock = $el.data('blockUI.onUnblock');
- $el.removeData('blockUI.onUnblock');
- }
-
- var els;
- if (full) // crazy selector to handle odd field errors in ie6/7
- els = $('body').children().filter('.blockUI').add('body > .blockUI');
- else
- els = $el.find('>.blockUI');
-
- // fix cursor issue
- if ( opts.cursorReset ) {
- if ( els.length > 1 )
- els[1].style.cursor = opts.cursorReset;
- if ( els.length > 2 )
- els[2].style.cursor = opts.cursorReset;
- }
-
- if (full)
- pageBlock = pageBlockEls = null;
-
- if (opts.fadeOut) {
- count = els.length;
- els.fadeOut(opts.fadeOut, function() {
- if ( --count === 0)
- reset(els,data,opts,el);
- });
- }
- else
- reset(els, data, opts, el);
- }
-
- // move blocking element back into the DOM where it started
- function reset(els,data,opts,el) {
- var $el = $(el);
- els.each(function(i,o) {
- // remove via DOM calls so we don't lose event handlers
- if (this.parentNode)
- this.parentNode.removeChild(this);
- });
-
- if (data && data.el) {
- data.el.style.display = data.display;
- data.el.style.position = data.position;
- if (data.parent)
- data.parent.appendChild(data.el);
- $el.removeData('blockUI.history');
- }
-
- if ($el.data('blockUI.static')) {
- $el.css('position', 'static'); // #22
- }
-
- if (typeof opts.onUnblock == 'function')
- opts.onUnblock(el,opts);
-
- // fix issue in Safari 6 where block artifacts remain until reflow
- var body = $(document.body), w = body.width(), cssW = body[0].style.width;
- body.width(w-1).width(w);
- body[0].style.width = cssW;
- }
-
- // bind/unbind the handler
- function bind(b, el, opts) {
- var full = el == window, $el = $(el);
-
- // don't bother unbinding if there is nothing to unbind
- if (!b && (full && !pageBlock || !full && !$el.data('blockUI.isBlocked')))
- return;
-
- $el.data('blockUI.isBlocked', b);
-
- // don't bind events when overlay is not in use or if bindEvents is false
- if (!full || !opts.bindEvents || (b && !opts.showOverlay))
- return;
-
- // bind anchors and inputs for mouse and key events
- var events = 'mousedown mouseup keydown keypress keyup touchstart touchend touchmove';
- if (b)
- $(document).bind(events, opts, handler);
- else
- $(document).unbind(events, handler);
-
- // former impl...
- // var $e = $('a,:input');
- // b ? $e.bind(events, opts, handler) : $e.unbind(events, handler);
- }
-
- // event handler to suppress keyboard/mouse events when blocking
- function handler(e) {
- // allow tab navigation (conditionally)
- if (e.keyCode && e.keyCode == 9) {
- if (pageBlock && e.data.constrainTabKey) {
- var els = pageBlockEls;
- var fwd = !e.shiftKey && e.target === els[els.length-1];
- var back = e.shiftKey && e.target === els[0];
- if (fwd || back) {
- setTimeout(function(){focus(back);},10);
- return false;
- }
- }
- }
- var opts = e.data;
- var target = $(e.target);
- if (target.hasClass('blockOverlay') && opts.onOverlayClick)
- opts.onOverlayClick();
-
- // allow events within the message content
- if (target.parents('div.' + opts.blockMsgClass).length > 0)
- return true;
-
- // allow events for content that is not being blocked
- return target.parents().children().filter('div.blockUI').length === 0;
- }
-
- function focus(back) {
- if (!pageBlockEls)
- return;
- var e = pageBlockEls[back===true ? pageBlockEls.length-1 : 0];
- if (e)
- e.focus();
- }
-
- function center(el, x, y) {
- var p = el.parentNode, s = el.style;
- var l = ((p.offsetWidth - el.offsetWidth)/2) - sz(p,'borderLeftWidth');
- var t = ((p.offsetHeight - el.offsetHeight)/2) - sz(p,'borderTopWidth');
- if (x) s.left = l > 0 ? (l+'px') : '0';
- if (y) s.top = t > 0 ? (t+'px') : '0';
- }
-
- function sz(el, p) {
- return parseInt($.css(el,p),10)||0;
- }
-
- }
-
-
- /*global define:true */
- if (typeof define === 'function' && define.amd && define.amd.jQuery) {
- define(['jquery'], setup);
- } else {
- setup(jQuery);
- }
-
-})();
diff --git a/vendor/assets/javascripts/jquery.history.js b/vendor/assets/javascripts/jquery.history.js
deleted file mode 100644
index 8d4edcd210e..00000000000
--- a/vendor/assets/javascripts/jquery.history.js
+++ /dev/null
@@ -1 +0,0 @@
-window.JSON||(window.JSON={}),function(){function f(a){return a<10?"0"+a:a}function quote(a){return escapable.lastIndex=0,escapable.test(a)?'"'+a.replace(escapable,function(a){var b=meta[a];return typeof b=="string"?b:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+a+'"'}function str(a,b){var c,d,e,f,g=gap,h,i=b[a];i&&typeof i=="object"&&typeof i.toJSON=="function"&&(i=i.toJSON(a)),typeof rep=="function"&&(i=rep.call(b,a,i));switch(typeof i){case"string":return quote(i);case"number":return isFinite(i)?String(i):"null";case"boolean":case"null":return String(i);case"object":if(!i)return"null";gap+=indent,h=[];if(Object.prototype.toString.apply(i)==="[object Array]"){f=i.length;for(c=0;c<f;c+=1)h[c]=str(c,i)||"null";return e=h.length===0?"[]":gap?"[\n"+gap+h.join(",\n"+gap)+"\n"+g+"]":"["+h.join(",")+"]",gap=g,e}if(rep&&typeof rep=="object"){f=rep.length;for(c=0;c<f;c+=1)d=rep[c],typeof d=="string"&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e))}else for(d in i)Object.hasOwnProperty.call(i,d)&&(e=str(d,i),e&&h.push(quote(d)+(gap?": ":":")+e));return e=h.length===0?"{}":gap?"{\n"+gap+h.join(",\n"+gap)+"\n"+g+"}":"{"+h.join(",")+"}",gap=g,e}}"use strict",typeof Date.prototype.toJSON!="function"&&(Date.prototype.toJSON=function(a){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null},String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(a){return this.valueOf()});var JSON=window.JSON,cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b","\t":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;typeof JSON.stringify!="function"&&(JSON.stringify=function(a,b,c){var d;gap="",indent="";if(typeof c=="number")for(d=0;d<c;d+=1)indent+=" ";else typeof c=="string"&&(indent=c);rep=b;if(!b||typeof b=="function"||typeof b=="object"&&typeof b.length=="number")return str("",{"":a});throw new Error("JSON.stringify")}),typeof JSON.parse!="function"&&(JSON.parse=function(text,reviver){function walk(a,b){var c,d,e=a[b];if(e&&typeof e=="object")for(c in e)Object.hasOwnProperty.call(e,c)&&(d=walk(e,c),d!==undefined?e[c]=d:delete e[c]);return reviver.call(a,b,e)}var j;text=String(text),cx.lastIndex=0,cx.test(text)&&(text=text.replace(cx,function(a){return"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)}));if(/^[\],:{}\s]*$/.test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,"@").replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,"]").replace(/(?:^|:|,)(?:\s*\[)+/g,"")))return j=eval("("+text+")"),typeof reviver=="function"?walk({"":j},""):j;throw new SyntaxError("JSON.parse")})}(),function(a,b){"use strict";var c=a.History=a.History||{},d=a.jQuery;if(typeof c.Adapter!="undefined")throw new Error("History.js Adapter has already been loaded...");c.Adapter={bind:function(a,b,c){d(a).bind(b,c)},trigger:function(a,b,c){d(a).trigger(b,c)},extractEventData:function(a,c,d){var e=c&&c.originalEvent&&c.originalEvent[a]||d&&d[a]||b;return e},onDomLoad:function(a){d(a)}},typeof c.init!="undefined"&&c.init()}(window),function(a,b){"use strict";var c=a.document,d=a.setTimeout||d,e=a.clearTimeout||e,f=a.setInterval||f,g=a.History=a.History||{};if(typeof g.initHtml4!="undefined")throw new Error("History.js HTML4 Support has already been loaded...");g.initHtml4=function(){if(typeof g.initHtml4.initialized!="undefined")return!1;g.initHtml4.initialized=!0,g.enabled=!0,g.savedHashes=[],g.isLastHash=function(a){var b=g.getHashByIndex(),c;return c=a===b,c},g.saveHash=function(a){return g.isLastHash(a)?!1:(g.savedHashes.push(a),!0)},g.getHashByIndex=function(a){var b=null;return typeof a=="undefined"?b=g.savedHashes[g.savedHashes.length-1]:a<0?b=g.savedHashes[g.savedHashes.length+a]:b=g.savedHashes[a],b},g.discardedHashes={},g.discardedStates={},g.discardState=function(a,b,c){var d=g.getHashByState(a),e;return e={discardedState:a,backState:c,forwardState:b},g.discardedStates[d]=e,!0},g.discardHash=function(a,b,c){var d={discardedHash:a,backState:c,forwardState:b};return g.discardedHashes[a]=d,!0},g.discardedState=function(a){var b=g.getHashByState(a),c;return c=g.discardedStates[b]||!1,c},g.discardedHash=function(a){var b=g.discardedHashes[a]||!1;return b},g.recycleState=function(a){var b=g.getHashByState(a);return g.discardedState(a)&&delete g.discardedStates[b],!0},g.emulated.hashChange&&(g.hashChangeInit=function(){g.checkerFunction=null;var b="",d,e,h,i;return g.isInternetExplorer()?(d="historyjs-iframe",e=c.createElement("iframe"),e.setAttribute("id",d),e.style.display="none",c.body.appendChild(e),e.contentWindow.document.open(),e.contentWindow.document.close(),h="",i=!1,g.checkerFunction=function(){if(i)return!1;i=!0;var c=g.getHash()||"",d=g.unescapeHash(e.contentWindow.document.location.hash)||"";return c!==b?(b=c,d!==c&&(h=d=c,e.contentWindow.document.open(),e.contentWindow.document.close(),e.contentWindow.document.location.hash=g.escapeHash(c)),g.Adapter.trigger(a,"hashchange")):d!==h&&(h=d,g.setHash(d,!1)),i=!1,!0}):g.checkerFunction=function(){var c=g.getHash();return c!==b&&(b=c,g.Adapter.trigger(a,"hashchange")),!0},g.intervalList.push(f(g.checkerFunction,g.options.hashChangeInterval)),!0},g.Adapter.onDomLoad(g.hashChangeInit)),g.emulated.pushState&&(g.onHashChange=function(b){var d=b&&b.newURL||c.location.href,e=g.getHashByUrl(d),f=null,h=null,i=null,j;return g.isLastHash(e)?(g.busy(!1),!1):(g.doubleCheckComplete(),g.saveHash(e),e&&g.isTraditionalAnchor(e)?(g.Adapter.trigger(a,"anchorchange"),g.busy(!1),!1):(f=g.extractState(g.getFullUrl(e||c.location.href,!1),!0),g.isLastSavedState(f)?(g.busy(!1),!1):(h=g.getHashByState(f),j=g.discardedState(f),j?(g.getHashByIndex(-2)===g.getHashByState(j.forwardState)?g.back(!1):g.forward(!1),!1):(g.pushState(f.data,f.title,f.url,!1),!0))))},g.Adapter.bind(a,"hashchange",g.onHashChange),g.pushState=function(b,d,e,f){if(g.getHashByUrl(e))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(f!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.pushState,args:arguments,queue:f}),!1;g.busy(!0);var h=g.createStateObject(b,d,e),i=g.getHashByState(h),j=g.getState(!1),k=g.getHashByState(j),l=g.getHash();return g.storeState(h),g.expectedStateId=h.id,g.recycleState(h),g.setTitle(h),i===k?(g.busy(!1),!1):i!==l&&i!==g.getShortUrl(c.location.href)?(g.setHash(i,!1),!1):(g.saveState(h),g.Adapter.trigger(a,"statechange"),g.busy(!1),!0)},g.replaceState=function(a,b,c,d){if(g.getHashByUrl(c))throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(d!==!1&&g.busy())return g.pushQueue({scope:g,callback:g.replaceState,args:arguments,queue:d}),!1;g.busy(!0);var e=g.createStateObject(a,b,c),f=g.getState(!1),h=g.getStateByIndex(-2);return g.discardState(f,e,h),g.pushState(e.data,e.title,e.url,!1),!0}),g.emulated.pushState&&g.getHash()&&!g.emulated.hashChange&&g.Adapter.onDomLoad(function(){g.Adapter.trigger(a,"hashchange")})},typeof g.init!="undefined"&&g.init()}(window),function(a,b){"use strict";var c=a.console||b,d=a.document,e=a.navigator,f=a.sessionStorage||!1,g=a.setTimeout,h=a.clearTimeout,i=a.setInterval,j=a.clearInterval,k=a.JSON,l=a.alert,m=a.History=a.History||{},n=a.history;k.stringify=k.stringify||k.encode,k.parse=k.parse||k.decode;if(typeof m.init!="undefined")throw new Error("History.js Core has already been loaded...");m.init=function(){return typeof m.Adapter=="undefined"?!1:(typeof m.initCore!="undefined"&&m.initCore(),typeof m.initHtml4!="undefined"&&m.initHtml4(),!0)},m.initCore=function(){if(typeof m.initCore.initialized!="undefined")return!1;m.initCore.initialized=!0,m.options=m.options||{},m.options.hashChangeInterval=m.options.hashChangeInterval||100,m.options.safariPollInterval=m.options.safariPollInterval||500,m.options.doubleCheckInterval=m.options.doubleCheckInterval||500,m.options.storeInterval=m.options.storeInterval||1e3,m.options.busyDelay=m.options.busyDelay||250,m.options.debug=m.options.debug||!1,m.options.initialTitle=m.options.initialTitle||d.title,m.intervalList=[],m.clearAllIntervals=function(){var a,b=m.intervalList;if(typeof b!="undefined"&&b!==null){for(a=0;a<b.length;a++)j(b[a]);m.intervalList=null}},m.debug=function(){(m.options.debug||!1)&&m.log.apply(m,arguments)},m.log=function(){var a=typeof c!="undefined"&&typeof c.log!="undefined"&&typeof c.log.apply!="undefined",b=d.getElementById("log"),e,f,g,h,i;a?(h=Array.prototype.slice.call(arguments),e=h.shift(),typeof c.debug!="undefined"?c.debug.apply(c,[e,h]):c.log.apply(c,[e,h])):e="\n"+arguments[0]+"\n";for(f=1,g=arguments.length;f<g;++f){i=arguments[f];if(typeof i=="object"&&typeof k!="undefined")try{i=k.stringify(i)}catch(j){}e+="\n"+i+"\n"}return b?(b.value+=e+"\n-----\n",b.scrollTop=b.scrollHeight-b.clientHeight):a||l(e),!0},m.getInternetExplorerMajorVersion=function(){var a=m.getInternetExplorerMajorVersion.cached=typeof m.getInternetExplorerMajorVersion.cached!="undefined"?m.getInternetExplorerMajorVersion.cached:function(){var a=3,b=d.createElement("div"),c=b.getElementsByTagName("i");while((b.innerHTML="<!--[if gt IE "+ ++a+"]><i></i><![endif]-->")&&c[0]);return a>4?a:!1}();return a},m.isInternetExplorer=function(){var a=m.isInternetExplorer.cached=typeof m.isInternetExplorer.cached!="undefined"?m.isInternetExplorer.cached:Boolean(m.getInternetExplorerMajorVersion());return a},m.emulated={pushState:!Boolean(a.history&&a.history.pushState&&a.history.replaceState&&!/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i.test(e.userAgent)&&!/AppleWebKit\/5([0-2]|3[0-2])/i.test(e.userAgent)),hashChange:Boolean(!("onhashchange"in a||"onhashchange"in d)||m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8)},m.enabled=!m.emulated.pushState,m.bugs={setHash:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),safariPoll:Boolean(!m.emulated.pushState&&e.vendor==="Apple Computer, Inc."&&/AppleWebKit\/5([0-2]|3[0-3])/.test(e.userAgent)),ieDoubleCheck:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<8),hashEscape:Boolean(m.isInternetExplorer()&&m.getInternetExplorerMajorVersion()<7)},m.isEmptyObject=function(a){for(var b in a)return!1;return!0},m.cloneObject=function(a){var b,c;return a?(b=k.stringify(a),c=k.parse(b)):c={},c},m.getRootUrl=function(){var a=d.location.protocol+"//"+(d.location.hostname||d.location.host);if(d.location.port||!1)a+=":"+d.location.port;return a+="/",a},m.getBaseHref=function(){var a=d.getElementsByTagName("base"),b=null,c="";return a.length===1&&(b=a[0],c=b.href.replace(/[^\/]+$/,"")),c=c.replace(/\/+$/,""),c&&(c+="/"),c},m.getBaseUrl=function(){var a=m.getBaseHref()||m.getBasePageUrl()||m.getRootUrl();return a},m.getPageUrl=function(){var a=m.getState(!1,!1),b=(a||{}).url||d.location.href,c;return c=b.replace(/\/+$/,"").replace(/[^\/]+$/,function(a,b,c){return/\./.test(a)?a:a+"/"}),c},m.getBasePageUrl=function(){var a=d.location.href.replace(/[#\?].*/,"").replace(/[^\/]+$/,function(a,b,c){return/[^\/]$/.test(a)?"":a}).replace(/\/+$/,"")+"/";return a},m.getFullUrl=function(a,b){var c=a,d=a.substring(0,1);return b=typeof b=="undefined"?!0:b,/[a-z]+\:\/\//.test(a)||(d==="/"?c=m.getRootUrl()+a.replace(/^\/+/,""):d==="#"?c=m.getPageUrl().replace(/#.*/,"")+a:d==="?"?c=m.getPageUrl().replace(/[\?#].*/,"")+a:b?c=m.getBaseUrl()+a.replace(/^(\.\/)+/,""):c=m.getBasePageUrl()+a.replace(/^(\.\/)+/,"")),c.replace(/\#$/,"")},m.getShortUrl=function(a){var b=a,c=m.getBaseUrl(),d=m.getRootUrl();return m.emulated.pushState&&(b=b.replace(c,"")),b=b.replace(d,"/"),m.isTraditionalAnchor(b)&&(b="./"+b),b=b.replace(/^(\.\/)+/g,"./").replace(/\#$/,""),b},m.store={},m.idToState=m.idToState||{},m.stateToId=m.stateToId||{},m.urlToId=m.urlToId||{},m.storedStates=m.storedStates||[],m.savedStates=m.savedStates||[],m.normalizeStore=function(){m.store.idToState=m.store.idToState||{},m.store.urlToId=m.store.urlToId||{},m.store.stateToId=m.store.stateToId||{}},m.getState=function(a,b){typeof a=="undefined"&&(a=!0),typeof b=="undefined"&&(b=!0);var c=m.getLastSavedState();return!c&&b&&(c=m.createStateObject()),a&&(c=m.cloneObject(c),c.url=c.cleanUrl||c.url),c},m.getIdByState=function(a){var b=m.extractId(a.url),c;if(!b){c=m.getStateString(a);if(typeof m.stateToId[c]!="undefined")b=m.stateToId[c];else if(typeof m.store.stateToId[c]!="undefined")b=m.store.stateToId[c];else{for(;;){b=(new Date).getTime()+String(Math.random()).replace(/\D/g,"");if(typeof m.idToState[b]=="undefined"&&typeof m.store.idToState[b]=="undefined")break}m.stateToId[c]=b,m.idToState[b]=a}}return b},m.normalizeState=function(a){var b,c;if(!a||typeof a!="object")a={};if(typeof a.normalized!="undefined")return a;if(!a.data||typeof a.data!="object")a.data={};b={},b.normalized=!0,b.title=a.title||"",b.url=m.getFullUrl(m.unescapeString(a.url||d.location.href)),b.hash=m.getShortUrl(b.url),b.data=m.cloneObject(a.data),b.id=m.getIdByState(b),b.cleanUrl=b.url.replace(/\??\&_suid.*/,""),b.url=b.cleanUrl,c=!m.isEmptyObject(b.data);if(b.title||c)b.hash=m.getShortUrl(b.url).replace(/\??\&_suid.*/,""),/\?/.test(b.hash)||(b.hash+="?"),b.hash+="&_suid="+b.id;return b.hashedUrl=m.getFullUrl(b.hash),(m.emulated.pushState||m.bugs.safariPoll)&&m.hasUrlDuplicate(b)&&(b.url=b.hashedUrl),b},m.createStateObject=function(a,b,c){var d={data:a,title:b,url:c};return d=m.normalizeState(d),d},m.getStateById=function(a){a=String(a);var c=m.idToState[a]||m.store.idToState[a]||b;return c},m.getStateString=function(a){var b,c,d;return b=m.normalizeState(a),c={data:b.data,title:a.title,url:a.url},d=k.stringify(c),d},m.getStateId=function(a){var b,c;return b=m.normalizeState(a),c=b.id,c},m.getHashByState=function(a){var b,c;return b=m.normalizeState(a),c=b.hash,c},m.extractId=function(a){var b,c,d;return c=/(.*)\&_suid=([0-9]+)$/.exec(a),d=c?c[1]||a:a,b=c?String(c[2]||""):"",b||!1},m.isTraditionalAnchor=function(a){var b=!/[\/\?\.]/.test(a);return b},m.extractState=function(a,b){var c=null,d,e;return b=b||!1,d=m.extractId(a),d&&(c=m.getStateById(d)),c||(e=m.getFullUrl(a),d=m.getIdByUrl(e)||!1,d&&(c=m.getStateById(d)),!c&&b&&!m.isTraditionalAnchor(a)&&(c=m.createStateObject(null,null,e))),c},m.getIdByUrl=function(a){var c=m.urlToId[a]||m.store.urlToId[a]||b;return c},m.getLastSavedState=function(){return m.savedStates[m.savedStates.length-1]||b},m.getLastStoredState=function(){return m.storedStates[m.storedStates.length-1]||b},m.hasUrlDuplicate=function(a){var b=!1,c;return c=m.extractState(a.url),b=c&&c.id!==a.id,b},m.storeState=function(a){return m.urlToId[a.url]=a.id,m.storedStates.push(m.cloneObject(a)),a},m.isLastSavedState=function(a){var b=!1,c,d,e;return m.savedStates.length&&(c=a.id,d=m.getLastSavedState(),e=d.id,b=c===e),b},m.saveState=function(a){return m.isLastSavedState(a)?!1:(m.savedStates.push(m.cloneObject(a)),!0)},m.getStateByIndex=function(a){var b=null;return typeof a=="undefined"?b=m.savedStates[m.savedStates.length-1]:a<0?b=m.savedStates[m.savedStates.length+a]:b=m.savedStates[a],b},m.getHash=function(){var a=m.unescapeHash(d.location.hash);return a},m.unescapeString=function(b){var c=b,d;for(;;){d=a.unescape(c);if(d===c)break;c=d}return c},m.unescapeHash=function(a){var b=m.normalizeHash(a);return b=m.unescapeString(b),b},m.normalizeHash=function(a){var b=a.replace(/[^#]*#/,"").replace(/#.*/,"");return b},m.setHash=function(a,b){var c,e,f;return b!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.setHash,args:arguments,queue:b}),!1):(c=m.escapeHash(a),m.busy(!0),e=m.extractState(a,!0),e&&!m.emulated.pushState?m.pushState(e.data,e.title,e.url,!1):d.location.hash!==c&&(m.bugs.setHash?(f=m.getPageUrl(),m.pushState(null,null,f+"#"+c,!1)):d.location.hash=c),m)},m.escapeHash=function(b){var c=m.normalizeHash(b);return c=a.escape(c),m.bugs.hashEscape||(c=c.replace(/\%21/g,"!").replace(/\%26/g,"&").replace(/\%3D/g,"=").replace(/\%3F/g,"?")),c},m.getHashByUrl=function(a){var b=String(a).replace(/([^#]*)#?([^#]*)#?(.*)/,"$2");return b=m.unescapeHash(b),b},m.setTitle=function(a){var b=a.title,c;b||(c=m.getStateByIndex(0),c&&c.url===a.url&&(b=c.title||m.options.initialTitle));try{d.getElementsByTagName("title")[0].innerHTML=b.replace("<","&lt;").replace(">","&gt;").replace(" & "," &amp; ")}catch(e){}return d.title=b,m},m.queues=[],m.busy=function(a){typeof a!="undefined"?m.busy.flag=a:typeof m.busy.flag=="undefined"&&(m.busy.flag=!1);if(!m.busy.flag){h(m.busy.timeout);var b=function(){var a,c,d;if(m.busy.flag)return;for(a=m.queues.length-1;a>=0;--a){c=m.queues[a];if(c.length===0)continue;d=c.shift(),m.fireQueueItem(d),m.busy.timeout=g(b,m.options.busyDelay)}};m.busy.timeout=g(b,m.options.busyDelay)}return m.busy.flag},m.busy.flag=!1,m.fireQueueItem=function(a){return a.callback.apply(a.scope||m,a.args||[])},m.pushQueue=function(a){return m.queues[a.queue||0]=m.queues[a.queue||0]||[],m.queues[a.queue||0].push(a),m},m.queue=function(a,b){return typeof a=="function"&&(a={callback:a}),typeof b!="undefined"&&(a.queue=b),m.busy()?m.pushQueue(a):m.fireQueueItem(a),m},m.clearQueue=function(){return m.busy.flag=!1,m.queues=[],m},m.stateChanged=!1,m.doubleChecker=!1,m.doubleCheckComplete=function(){return m.stateChanged=!0,m.doubleCheckClear(),m},m.doubleCheckClear=function(){return m.doubleChecker&&(h(m.doubleChecker),m.doubleChecker=!1),m},m.doubleCheck=function(a){return m.stateChanged=!1,m.doubleCheckClear(),m.bugs.ieDoubleCheck&&(m.doubleChecker=g(function(){return m.doubleCheckClear(),m.stateChanged||a(),!0},m.options.doubleCheckInterval)),m},m.safariStatePoll=function(){var b=m.extractState(d.location.href),c;if(!m.isLastSavedState(b))c=b;else return;return c||(c=m.createStateObject()),m.Adapter.trigger(a,"popstate"),m},m.back=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.back,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.back(!1)}),n.go(-1),!0)},m.forward=function(a){return a!==!1&&m.busy()?(m.pushQueue({scope:m,callback:m.forward,args:arguments,queue:a}),!1):(m.busy(!0),m.doubleCheck(function(){m.forward(!1)}),n.go(1),!0)},m.go=function(a,b){var c;if(a>0)for(c=1;c<=a;++c)m.forward(b);else{if(!(a<0))throw new Error("History.go: History.go requires a positive or negative integer passed.");for(c=-1;c>=a;--c)m.back(b)}return m};if(m.emulated.pushState){var o=function(){};m.pushState=m.pushState||o,m.replaceState=m.replaceState||o}else m.onPopState=function(b,c){var e=!1,f=!1,g,h;return m.doubleCheckComplete(),g=m.getHash(),g?(h=m.extractState(g||d.location.href,!0),h?m.replaceState(h.data,h.title,h.url,!1):(m.Adapter.trigger(a,"anchorchange"),m.busy(!1)),m.expectedStateId=!1,!1):(e=m.Adapter.extractEventData("state",b,c)||!1,e?f=m.getStateById(e):m.expectedStateId?f=m.getStateById(m.expectedStateId):f=m.extractState(d.location.href),f||(f=m.createStateObject(null,null,d.location.href)),m.expectedStateId=!1,m.isLastSavedState(f)?(m.busy(!1),!1):(m.storeState(f),m.saveState(f),m.setTitle(f),m.Adapter.trigger(a,"statechange"),m.busy(!1),!0))},m.Adapter.bind(a,"popstate",m.onPopState),m.pushState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.pushState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.pushState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0},m.replaceState=function(b,c,d,e){if(m.getHashByUrl(d)&&m.emulated.pushState)throw new Error("History.js does not support states with fragement-identifiers (hashes/anchors).");if(e!==!1&&m.busy())return m.pushQueue({scope:m,callback:m.replaceState,args:arguments,queue:e}),!1;m.busy(!0);var f=m.createStateObject(b,c,d);return m.isLastSavedState(f)?m.busy(!1):(m.storeState(f),m.expectedStateId=f.id,n.replaceState(f.id,f.title,f.url),m.Adapter.trigger(a,"popstate")),!0};if(f){try{m.store=k.parse(f.getItem("History.store"))||{}}catch(p){m.store={}}m.normalizeStore()}else m.store={},m.normalizeStore();m.Adapter.bind(a,"beforeunload",m.clearAllIntervals),m.Adapter.bind(a,"unload",m.clearAllIntervals),m.saveState(m.storeState(m.extractState(d.location.href,!0))),f&&(m.onUnload=function(){var a,b;try{a=k.parse(f.getItem("History.store"))||{}}catch(c){a={}}a.idToState=a.idToState||{},a.urlToId=a.urlToId||{},a.stateToId=a.stateToId||{};for(b in m.idToState){if(!m.idToState.hasOwnProperty(b))continue;a.idToState[b]=m.idToState[b]}for(b in m.urlToId){if(!m.urlToId.hasOwnProperty(b))continue;a.urlToId[b]=m.urlToId[b]}for(b in m.stateToId){if(!m.stateToId.hasOwnProperty(b))continue;a.stateToId[b]=m.stateToId[b]}m.store=a,m.normalizeStore(),f.setItem("History.store",k.stringify(a))},m.intervalList.push(i(m.onUnload,m.options.storeInterval)),m.Adapter.bind(a,"beforeunload",m.onUnload),m.Adapter.bind(a,"unload",m.onUnload));if(!m.emulated.pushState){m.bugs.safariPoll&&m.intervalList.push(i(m.safariStatePoll,m.options.safariPollInterval));if(e.vendor==="Apple Computer, Inc."||(e.appCodeName||"")==="Mozilla")m.Adapter.bind(a,"hashchange",function(){m.Adapter.trigger(a,"popstate")}),m.getHash()&&m.Adapter.onDomLoad(function(){m.Adapter.trigger(a,"hashchange")})}},m.init()}(window) \ No newline at end of file
diff --git a/vendor/assets/javascripts/latinise.js b/vendor/assets/javascripts/latinise.js
new file mode 100644
index 00000000000..da37966b28a
--- /dev/null
+++ b/vendor/assets/javascripts/latinise.js
@@ -0,0 +1,11 @@
+// Converting text to basic latin (aka removing accents)
+//
+// Based on: http://semplicewebsites.com/removing-accents-javascript
+//
+var Latinise = {
+ map: {"Á":"A","Ă":"A","Ắ":"A","Ặ":"A","Ằ":"A","Ẳ":"A","Ẵ":"A","Ǎ":"A","Â":"A","Ấ":"A","Ậ":"A","Ầ":"A","Ẩ":"A","Ẫ":"A","Ä":"A","Ǟ":"A","Ȧ":"A","Ǡ":"A","Ạ":"A","Ȁ":"A","À":"A","Ả":"A","Ȃ":"A","Ā":"A","Ą":"A","Å":"A","Ǻ":"A","Ḁ":"A","Ⱥ":"A","Ã":"A","Ꜳ":"AA","Æ":"AE","Ǽ":"AE","Ǣ":"AE","Ꜵ":"AO","Ꜷ":"AU","Ꜹ":"AV","Ꜻ":"AV","Ꜽ":"AY","Ḃ":"B","Ḅ":"B","Ɓ":"B","Ḇ":"B","Ƀ":"B","Ƃ":"B","Ć":"C","Č":"C","Ç":"C","Ḉ":"C","Ĉ":"C","Ċ":"C","Ƈ":"C","Ȼ":"C","Ď":"D","Ḑ":"D","Ḓ":"D","Ḋ":"D","Ḍ":"D","Ɗ":"D","Ḏ":"D","Dz":"D","Dž":"D","Đ":"D","Ƌ":"D","DZ":"DZ","DŽ":"DZ","É":"E","Ĕ":"E","Ě":"E","Ȩ":"E","Ḝ":"E","Ê":"E","Ế":"E","Ệ":"E","Ề":"E","Ể":"E","Ễ":"E","Ḙ":"E","Ë":"E","Ė":"E","Ẹ":"E","Ȅ":"E","È":"E","Ẻ":"E","Ȇ":"E","Ē":"E","Ḗ":"E","Ḕ":"E","Ę":"E","Ɇ":"E","Ẽ":"E","Ḛ":"E","Ꝫ":"ET","Ḟ":"F","Ƒ":"F","Ǵ":"G","Ğ":"G","Ǧ":"G","Ģ":"G","Ĝ":"G","Ġ":"G","Ɠ":"G","Ḡ":"G","Ǥ":"G","Ḫ":"H","Ȟ":"H","Ḩ":"H","Ĥ":"H","Ⱨ":"H","Ḧ":"H","Ḣ":"H","Ḥ":"H","Ħ":"H","Í":"I","Ĭ":"I","Ǐ":"I","Î":"I","Ï":"I","Ḯ":"I","İ":"I","Ị":"I","Ȉ":"I","Ì":"I","Ỉ":"I","Ȋ":"I","Ī":"I","Į":"I","Ɨ":"I","Ĩ":"I","Ḭ":"I","Ꝺ":"D","Ꝼ":"F","Ᵹ":"G","Ꞃ":"R","Ꞅ":"S","Ꞇ":"T","Ꝭ":"IS","Ĵ":"J","Ɉ":"J","Ḱ":"K","Ǩ":"K","Ķ":"K","Ⱪ":"K","Ꝃ":"K","Ḳ":"K","Ƙ":"K","Ḵ":"K","Ꝁ":"K","Ꝅ":"K","Ĺ":"L","Ƚ":"L","Ľ":"L","Ļ":"L","Ḽ":"L","Ḷ":"L","Ḹ":"L","Ⱡ":"L","Ꝉ":"L","Ḻ":"L","Ŀ":"L","Ɫ":"L","Lj":"L","Ł":"L","LJ":"LJ","Ḿ":"M","Ṁ":"M","Ṃ":"M","Ɱ":"M","Ń":"N","Ň":"N","Ņ":"N","Ṋ":"N","Ṅ":"N","Ṇ":"N","Ǹ":"N","Ɲ":"N","Ṉ":"N","Ƞ":"N","Nj":"N","Ñ":"N","NJ":"NJ","Ó":"O","Ŏ":"O","Ǒ":"O","Ô":"O","Ố":"O","Ộ":"O","Ồ":"O","Ổ":"O","Ỗ":"O","Ö":"O","Ȫ":"O","Ȯ":"O","Ȱ":"O","Ọ":"O","Ő":"O","Ȍ":"O","Ò":"O","Ỏ":"O","Ơ":"O","Ớ":"O","Ợ":"O","Ờ":"O","Ở":"O","Ỡ":"O","Ȏ":"O","Ꝋ":"O","Ꝍ":"O","Ō":"O","Ṓ":"O","Ṑ":"O","Ɵ":"O","Ǫ":"O","Ǭ":"O","Ø":"O","Ǿ":"O","Õ":"O","Ṍ":"O","Ṏ":"O","Ȭ":"O","Ƣ":"OI","Ꝏ":"OO","Ɛ":"E","Ɔ":"O","Ȣ":"OU","Ṕ":"P","Ṗ":"P","Ꝓ":"P","Ƥ":"P","Ꝕ":"P","Ᵽ":"P","Ꝑ":"P","Ꝙ":"Q","Ꝗ":"Q","Ŕ":"R","Ř":"R","Ŗ":"R","Ṙ":"R","Ṛ":"R","Ṝ":"R","Ȑ":"R","Ȓ":"R","Ṟ":"R","Ɍ":"R","Ɽ":"R","Ꜿ":"C","Ǝ":"E","Ś":"S","Ṥ":"S","Š":"S","Ṧ":"S","Ş":"S","Ŝ":"S","Ș":"S","Ṡ":"S","Ṣ":"S","Ṩ":"S","ẞ":"SS","Ť":"T","Ţ":"T","Ṱ":"T","Ț":"T","Ⱦ":"T","Ṫ":"T","Ṭ":"T","Ƭ":"T","Ṯ":"T","Ʈ":"T","Ŧ":"T","Ɐ":"A","Ꞁ":"L","Ɯ":"M","Ʌ":"V","Ꜩ":"TZ","Ú":"U","Ŭ":"U","Ǔ":"U","Û":"U","Ṷ":"U","Ü":"U","Ǘ":"U","Ǚ":"U","Ǜ":"U","Ǖ":"U","Ṳ":"U","Ụ":"U","Ű":"U","Ȕ":"U","Ù":"U","Ủ":"U","Ư":"U","Ứ":"U","Ự":"U","Ừ":"U","Ử":"U","Ữ":"U","Ȗ":"U","Ū":"U","Ṻ":"U","Ų":"U","Ů":"U","Ũ":"U","Ṹ":"U","Ṵ":"U","Ꝟ":"V","Ṿ":"V","Ʋ":"V","Ṽ":"V","Ꝡ":"VY","Ẃ":"W","Ŵ":"W","Ẅ":"W","Ẇ":"W","Ẉ":"W","Ẁ":"W","Ⱳ":"W","Ẍ":"X","Ẋ":"X","Ý":"Y","Ŷ":"Y","Ÿ":"Y","Ẏ":"Y","Ỵ":"Y","Ỳ":"Y","Ƴ":"Y","Ỷ":"Y","Ỿ":"Y","Ȳ":"Y","Ɏ":"Y","Ỹ":"Y","Ź":"Z","Ž":"Z","Ẑ":"Z","Ⱬ":"Z","Ż":"Z","Ẓ":"Z","Ȥ":"Z","Ẕ":"Z","Ƶ":"Z","IJ":"IJ","Œ":"OE","ᴀ":"A","ᴁ":"AE","ʙ":"B","ᴃ":"B","ᴄ":"C","ᴅ":"D","ᴇ":"E","ꜰ":"F","ɢ":"G","ʛ":"G","ʜ":"H","ɪ":"I","ʁ":"R","ᴊ":"J","ᴋ":"K","ʟ":"L","ᴌ":"L","ᴍ":"M","ɴ":"N","ᴏ":"O","ɶ":"OE","ᴐ":"O","ᴕ":"OU","ᴘ":"P","ʀ":"R","ᴎ":"N","ᴙ":"R","ꜱ":"S","ᴛ":"T","ⱻ":"E","ᴚ":"R","ᴜ":"U","ᴠ":"V","ᴡ":"W","ʏ":"Y","ᴢ":"Z","á":"a","ă":"a","ắ":"a","ặ":"a","ằ":"a","ẳ":"a","ẵ":"a","ǎ":"a","â":"a","ấ":"a","ậ":"a","ầ":"a","ẩ":"a","ẫ":"a","ä":"a","ǟ":"a","ȧ":"a","ǡ":"a","ạ":"a","ȁ":"a","à":"a","ả":"a","ȃ":"a","ā":"a","ą":"a","ᶏ":"a","ẚ":"a","å":"a","ǻ":"a","ḁ":"a","ⱥ":"a","ã":"a","ꜳ":"aa","æ":"ae","ǽ":"ae","ǣ":"ae","ꜵ":"ao","ꜷ":"au","ꜹ":"av","ꜻ":"av","ꜽ":"ay","ḃ":"b","ḅ":"b","ɓ":"b","ḇ":"b","ᵬ":"b","ᶀ":"b","ƀ":"b","ƃ":"b","ɵ":"o","ć":"c","č":"c","ç":"c","ḉ":"c","ĉ":"c","ɕ":"c","ċ":"c","ƈ":"c","ȼ":"c","ď":"d","ḑ":"d","ḓ":"d","ȡ":"d","ḋ":"d","ḍ":"d","ɗ":"d","ᶑ":"d","ḏ":"d","ᵭ":"d","ᶁ":"d","đ":"d","ɖ":"d","ƌ":"d","ı":"i","ȷ":"j","ɟ":"j","ʄ":"j","dz":"dz","dž":"dz","é":"e","ĕ":"e","ě":"e","ȩ":"e","ḝ":"e","ê":"e","ế":"e","ệ":"e","ề":"e","ể":"e","ễ":"e","ḙ":"e","ë":"e","ė":"e","ẹ":"e","ȅ":"e","è":"e","ẻ":"e","ȇ":"e","ē":"e","ḗ":"e","ḕ":"e","ⱸ":"e","ę":"e","ᶒ":"e","ɇ":"e","ẽ":"e","ḛ":"e","ꝫ":"et","ḟ":"f","ƒ":"f","ᵮ":"f","ᶂ":"f","ǵ":"g","ğ":"g","ǧ":"g","ģ":"g","ĝ":"g","ġ":"g","ɠ":"g","ḡ":"g","ᶃ":"g","ǥ":"g","ḫ":"h","ȟ":"h","ḩ":"h","ĥ":"h","ⱨ":"h","ḧ":"h","ḣ":"h","ḥ":"h","ɦ":"h","ẖ":"h","ħ":"h","ƕ":"hv","í":"i","ĭ":"i","ǐ":"i","î":"i","ï":"i","ḯ":"i","ị":"i","ȉ":"i","ì":"i","ỉ":"i","ȋ":"i","ī":"i","į":"i","ᶖ":"i","ɨ":"i","ĩ":"i","ḭ":"i","ꝺ":"d","ꝼ":"f","ᵹ":"g","ꞃ":"r","ꞅ":"s","ꞇ":"t","ꝭ":"is","ǰ":"j","ĵ":"j","ʝ":"j","ɉ":"j","ḱ":"k","ǩ":"k","ķ":"k","ⱪ":"k","ꝃ":"k","ḳ":"k","ƙ":"k","ḵ":"k","ᶄ":"k","ꝁ":"k","ꝅ":"k","ĺ":"l","ƚ":"l","ɬ":"l","ľ":"l","ļ":"l","ḽ":"l","ȴ":"l","ḷ":"l","ḹ":"l","ⱡ":"l","ꝉ":"l","ḻ":"l","ŀ":"l","ɫ":"l","ᶅ":"l","ɭ":"l","ł":"l","lj":"lj","ſ":"s","ẜ":"s","ẛ":"s","ẝ":"s","ḿ":"m","ṁ":"m","ṃ":"m","ɱ":"m","ᵯ":"m","ᶆ":"m","ń":"n","ň":"n","ņ":"n","ṋ":"n","ȵ":"n","ṅ":"n","ṇ":"n","ǹ":"n","ɲ":"n","ṉ":"n","ƞ":"n","ᵰ":"n","ᶇ":"n","ɳ":"n","ñ":"n","nj":"nj","ó":"o","ŏ":"o","ǒ":"o","ô":"o","ố":"o","ộ":"o","ồ":"o","ổ":"o","ỗ":"o","ö":"o","ȫ":"o","ȯ":"o","ȱ":"o","ọ":"o","ő":"o","ȍ":"o","ò":"o","ỏ":"o","ơ":"o","ớ":"o","ợ":"o","ờ":"o","ở":"o","ỡ":"o","ȏ":"o","ꝋ":"o","ꝍ":"o","ⱺ":"o","ō":"o","ṓ":"o","ṑ":"o","ǫ":"o","ǭ":"o","ø":"o","ǿ":"o","õ":"o","ṍ":"o","ṏ":"o","ȭ":"o","ƣ":"oi","ꝏ":"oo","ɛ":"e","ᶓ":"e","ɔ":"o","ᶗ":"o","ȣ":"ou","ṕ":"p","ṗ":"p","ꝓ":"p","ƥ":"p","ᵱ":"p","ᶈ":"p","ꝕ":"p","ᵽ":"p","ꝑ":"p","ꝙ":"q","ʠ":"q","ɋ":"q","ꝗ":"q","ŕ":"r","ř":"r","ŗ":"r","ṙ":"r","ṛ":"r","ṝ":"r","ȑ":"r","ɾ":"r","ᵳ":"r","ȓ":"r","ṟ":"r","ɼ":"r","ᵲ":"r","ᶉ":"r","ɍ":"r","ɽ":"r","ↄ":"c","ꜿ":"c","ɘ":"e","ɿ":"r","ś":"s","ṥ":"s","š":"s","ṧ":"s","ş":"s","ŝ":"s","ș":"s","ṡ":"s","ṣ":"s","ṩ":"s","ʂ":"s","ᵴ":"s","ᶊ":"s","ȿ":"s","ɡ":"g","ß":"ss","ᴑ":"o","ᴓ":"o","ᴝ":"u","ť":"t","ţ":"t","ṱ":"t","ț":"t","ȶ":"t","ẗ":"t","ⱦ":"t","ṫ":"t","ṭ":"t","ƭ":"t","ṯ":"t","ᵵ":"t","ƫ":"t","ʈ":"t","ŧ":"t","ᵺ":"th","ɐ":"a","ᴂ":"ae","ǝ":"e","ᵷ":"g","ɥ":"h","ʮ":"h","ʯ":"h","ᴉ":"i","ʞ":"k","ꞁ":"l","ɯ":"m","ɰ":"m","ᴔ":"oe","ɹ":"r","ɻ":"r","ɺ":"r","ⱹ":"r","ʇ":"t","ʌ":"v","ʍ":"w","ʎ":"y","ꜩ":"tz","ú":"u","ŭ":"u","ǔ":"u","û":"u","ṷ":"u","ü":"u","ǘ":"u","ǚ":"u","ǜ":"u","ǖ":"u","ṳ":"u","ụ":"u","ű":"u","ȕ":"u","ù":"u","ủ":"u","ư":"u","ứ":"u","ự":"u","ừ":"u","ử":"u","ữ":"u","ȗ":"u","ū":"u","ṻ":"u","ų":"u","ᶙ":"u","ů":"u","ũ":"u","ṹ":"u","ṵ":"u","ᵫ":"ue","ꝸ":"um","ⱴ":"v","ꝟ":"v","ṿ":"v","ʋ":"v","ᶌ":"v","ⱱ":"v","ṽ":"v","ꝡ":"vy","ẃ":"w","ŵ":"w","ẅ":"w","ẇ":"w","ẉ":"w","ẁ":"w","ⱳ":"w","ẘ":"w","ẍ":"x","ẋ":"x","ᶍ":"x","ý":"y","ŷ":"y","ÿ":"y","ẏ":"y","ỵ":"y","ỳ":"y","ƴ":"y","ỷ":"y","ỿ":"y","ȳ":"y","ẙ":"y","ɏ":"y","ỹ":"y","ź":"z","ž":"z","ẑ":"z","ʑ":"z","ⱬ":"z","ż":"z","ẓ":"z","ȥ":"z","ẕ":"z","ᵶ":"z","ᶎ":"z","ʐ":"z","ƶ":"z","ɀ":"z","ff":"ff","ffi":"ffi","ffl":"ffl","fi":"fi","fl":"fl","ij":"ij","œ":"oe","st":"st","ₐ":"a","ₑ":"e","ᵢ":"i","ⱼ":"j","ₒ":"o","ᵣ":"r","ᵤ":"u","ᵥ":"v","ₓ":"x"}
+};
+
+String.prototype.latinise = function() {
+ return this.replace(/[^A-Za-z0-9]/g, function(x) { return Latinise.map[x] || x; });
+};