summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml2
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG106
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile9
-rw-r--r--Gemfile.lock16
-rw-r--r--LICENSE2
-rw-r--r--Procfile2
-rw-r--r--app/assets/javascripts/api.js.coffee67
-rw-r--r--app/assets/javascripts/application.js.coffee4
-rw-r--r--app/assets/javascripts/calendar.js.coffee6
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee4
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee15
-rw-r--r--app/assets/javascripts/issue.js.coffee12
-rw-r--r--app/assets/javascripts/merge_request.js.coffee21
-rw-r--r--app/assets/javascripts/notes.js.coffee10
-rw-r--r--app/assets/javascripts/project_users_select.js.coffee56
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee10
-rw-r--r--app/assets/javascripts/shortcuts_navigation.coffee22
-rw-r--r--app/assets/javascripts/sidebar.js.coffee11
-rw-r--r--app/assets/javascripts/users_select.js.coffee89
-rw-r--r--app/assets/stylesheets/base/mixins.scss16
-rw-r--r--app/assets/stylesheets/generic/common.scss2
-rw-r--r--app/assets/stylesheets/generic/filters.scss55
-rw-r--r--app/assets/stylesheets/generic/mobile.scss7
-rw-r--r--app/assets/stylesheets/generic/selects.scss18
-rw-r--r--app/assets/stylesheets/pages/events.scss3
-rw-r--r--app/assets/stylesheets/pages/issues.scss35
-rw-r--r--app/assets/stylesheets/pages/notes.scss16
-rw-r--r--app/assets/stylesheets/pages/projects.scss72
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb49
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/application_controller.rb5
-rw-r--r--app/controllers/autocomplete_controller.rb30
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/groups/application_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb21
-rw-r--r--app/controllers/groups/milestones_controller.rb2
-rw-r--r--app/controllers/help_controller.rb75
-rw-r--r--app/controllers/import/base_controller.rb2
-rw-r--r--app/controllers/import/bitbucket_controller.rb5
-rw-r--r--app/controllers/import/github_controller.rb5
-rw-r--r--app/controllers/import/gitlab_controller.rb5
-rw-r--r--app/controllers/import/google_code_controller.rb116
-rw-r--r--app/controllers/invites_controller.rb83
-rw-r--r--app/controllers/namespaces_controller.rb18
-rw-r--r--app/controllers/profiles/emails_controller.rb4
-rw-r--r--app/controllers/profiles_controller.rb7
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb29
-rw-r--r--app/controllers/projects/project_members_controller.rb32
-rw-r--r--app/controllers/projects/refs_controller.rb5
-rw-r--r--app/controllers/projects/repositories_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb6
-rw-r--r--app/controllers/search_controller.rb22
-rw-r--r--app/controllers/sessions_controller.rb8
-rw-r--r--app/finders/issuable_finder.rb8
-rw-r--r--app/helpers/application_helper.rb56
-rw-r--r--app/helpers/blob_helper.rb8
-rw-r--r--app/helpers/commits_helper.rb7
-rw-r--r--app/helpers/diff_helper.rb2
-rw-r--r--app/helpers/emails_helper.rb31
-rw-r--r--app/helpers/gitlab_markdown_helper.rb42
-rw-r--r--app/helpers/gitlab_routing_helper.rb4
-rw-r--r--app/helpers/groups_helper.rb8
-rw-r--r--app/helpers/icons_helper.rb44
-rw-r--r--app/helpers/issues_helper.rb24
-rw-r--r--app/helpers/labels_helper.rb4
-rw-r--r--app/helpers/milestones_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb50
-rw-r--r--app/helpers/selects_helper.rb33
-rw-r--r--app/helpers/submodule_helper.rb21
-rw-r--r--app/helpers/tree_helper.rb9
-rw-r--r--app/helpers/wiki_helper.rb22
-rw-r--r--app/mailers/emails/groups.rb43
-rw-r--r--app/mailers/emails/profile.rb6
-rw-r--r--app/mailers/emails/projects.rb66
-rw-r--r--app/mailers/notify.rb36
-rw-r--r--app/models/ability.rb12
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/commit.rb16
-rw-r--r--app/models/concerns/issuable.rb8
-rw-r--r--app/models/concerns/mentionable.rb37
-rw-r--r--app/models/deploy_key.rb18
-rw-r--r--app/models/deploy_keys_project.rb4
-rw-r--r--app/models/event.rb6
-rw-r--r--app/models/group.rb15
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/models/identity.rb1
-rw-r--r--app/models/key.rb20
-rw-r--r--app/models/member.rb140
-rw-r--r--app/models/members/group_member.rb33
-rw-r--r--app/models/members/project_member.rb58
-rw-r--r--app/models/merge_request.rb12
-rw-r--r--app/models/namespace.rb48
-rw-r--r--app/models/note.rb16
-rw-r--r--app/models/project.rb17
-rw-r--r--app/models/project_import_data.rb19
-rw-r--r--app/models/project_services/bamboo_service.rb2
-rw-r--r--app/models/project_services/buildkite_service.rb (renamed from app/models/project_services/buildbox_service.rb)4
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/gitlab_ci_service.rb28
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb25
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb12
-rw-r--r--app/models/project_services/teamcity_service.rb2
-rw-r--r--app/models/project_team.rb30
-rw-r--r--app/models/repository.rb7
-rw-r--r--app/models/service.rb28
-rw-r--r--app/models/snippet.rb4
-rw-r--r--app/models/user.rb43
-rw-r--r--app/services/archive_repository_service.rb64
-rw-r--r--app/services/create_tag_service.rb4
-rw-r--r--app/services/files/create_service.rb4
-rw-r--r--app/services/git_push_service.rb6
-rw-r--r--app/services/issues/update_service.rb4
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/notification_service.rb52
-rw-r--r--app/services/projects/create_service.rb2
-rw-r--r--app/services/projects/fork_service.rb8
-rw-r--r--app/services/projects/participants_service.rb19
-rw-r--r--app/services/projects/upload_service.rb8
-rw-r--r--app/views/admin/application_settings/_form.html.haml5
-rw-r--r--app/views/admin/applications/_form.html.haml10
-rw-r--r--app/views/admin/background_jobs/show.html.haml2
-rw-r--r--app/views/admin/deploy_keys/index.html.haml27
-rw-r--r--app/views/admin/deploy_keys/new.html.haml26
-rw-r--r--app/views/admin/deploy_keys/show.html.haml34
-rw-r--r--app/views/admin/groups/show.html.haml15
-rw-r--r--app/views/admin/projects/show.html.haml15
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/dashboard/groups/index.html.haml2
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml20
-rw-r--r--app/views/dashboard/milestones/index.html.haml20
-rw-r--r--app/views/dashboard/projects/starred.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml5
-rw-r--r--app/views/events/event/_created_project.html.haml4
-rw-r--r--app/views/groups/group_members/_group_member.html.haml35
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml5
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/milestones/_issue.html.haml2
-rw-r--r--app/views/groups/milestones/_merge_request.html.haml2
-rw-r--r--app/views/groups/milestones/_milestone.html.haml25
-rw-r--r--app/views/groups/milestones/index.html.haml26
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/import/base/create.js.haml2
-rw-r--r--app/views/import/google_code/new.html.haml60
-rw-r--r--app/views/import/google_code/new_user_map.html.haml20
-rw-r--r--app/views/import/google_code/status.html.haml49
-rw-r--r--app/views/invites/show.html.haml29
-rw-r--r--app/views/layouts/_head_panel.html.haml2
-rw-r--r--app/views/layouts/nav/_admin.html.haml5
-rw-r--r--app/views/layouts/nav/_group.html.haml2
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/layouts/notify.html.haml3
-rw-r--r--app/views/notify/_note_message.html.haml2
-rw-r--r--app/views/notify/group_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/group_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/group_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/group_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/group_member_invited_email.html.haml14
-rw-r--r--app/views/notify/group_member_invited_email.text.erb4
-rw-r--r--app/views/notify/new_issue_email.html.haml2
-rw-r--r--app/views/notify/new_merge_request_email.html.haml2
-rw-r--r--app/views/notify/project_invite_accepted_email.html.haml6
-rw-r--r--app/views/notify/project_invite_accepted_email.text.erb3
-rw-r--r--app/views/notify/project_invite_declined_email.html.haml5
-rw-r--r--app/views/notify/project_invite_declined_email.text.erb3
-rw-r--r--app/views/notify/project_member_invited_email.html.haml13
-rw-r--r--app/views/notify/project_member_invited_email.text.erb4
-rw-r--r--app/views/notify/repository_push_email.html.haml3
-rw-r--r--app/views/profiles/emails/index.html.haml4
-rw-r--r--app/views/profiles/show.html.haml5
-rw-r--r--app/views/projects/_dropdown.html.haml14
-rw-r--r--app/views/projects/_home_panel.html.haml35
-rw-r--r--app/views/projects/_issuable_form.html.haml4
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/commit/_commit_box.html.haml3
-rw-r--r--app/views/projects/commits/_head.html.haml4
-rw-r--r--app/views/projects/deploy_keys/_deploy_key.html.haml27
-rw-r--r--app/views/projects/deploy_keys/index.html.haml25
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/diffs/_text_file.html.haml2
-rw-r--r--app/views/projects/graphs/commits.html.haml6
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/issues/_issue_context.html.haml4
-rw-r--r--app/views/projects/issues/index.html.haml10
-rw-r--r--app/views/projects/issues/update.js.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_context.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_participants.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_state_widget.html.haml2
-rw-r--r--app/views/projects/merge_requests/update.js.haml2
-rw-r--r--app/views/projects/milestones/_issue.html.haml8
-rw-r--r--app/views/projects/milestones/_merge_request.html.haml2
-rw-r--r--app/views/projects/milestones/_milestone.html.haml22
-rw-r--r--app/views/projects/new.html.haml4
-rw-r--r--app/views/projects/notes/_discussion.html.haml7
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml5
-rw-r--r--app/views/projects/project_members/_project_member.html.haml38
-rw-r--r--app/views/projects/refs/logs_tree.js.haml2
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml6
-rw-r--r--app/views/projects/show.html.haml40
-rw-r--r--app/views/projects/tags/_tag.html.haml4
-rw-r--r--app/views/projects/tree/_blob_item.html.haml2
-rw-r--r--app/views/projects/tree/_submodule_item.html.haml2
-rw-r--r--app/views/projects/tree/_tree_item.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_issuable_filter.html.haml137
-rw-r--r--app/views/shared/_project.html.haml2
-rw-r--r--app/views/snippets/show.html.haml2
-rw-r--r--app/views/users/_profile.html.haml4
-rw-r--r--app/views/users/_projects.html.haml8
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/fork_registration_worker.rb12
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--app/workers/repository_archive_worker.rb43
-rw-r--r--app/workers/repository_import_worker.rb2
-rwxr-xr-xbin/background_jobs2
-rw-r--r--config/gitlab.yml.example48
-rw-r--r--config/initializers/1_settings.rb11
-rw-r--r--config/initializers/2_app.rb5
-rw-r--r--config/initializers/5_backend.rb7
-rw-r--r--config/initializers/acts_as_taggable_on_patch.rb131
-rw-r--r--config/initializers/devise.rb2
-rw-r--r--config/initializers/doorkeeper.rb4
-rw-r--r--config/initializers/mime_types.rb2
-rw-r--r--config/routes.rb43
-rw-r--r--db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb76
-rw-r--r--db/migrate/20150327122227_add_public_to_key.rb5
-rw-r--r--db/migrate/20150327150017_add_import_data_to_project.rb5
-rw-r--r--db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb5
-rw-r--r--db/migrate/20150406133311_add_invite_data_to_member.rb12
-rw-r--r--db/migrate/20150411000035_fix_identities.rb45
-rw-r--r--db/migrate/20150411180045_rename_buildbox_service.rb9
-rw-r--r--db/migrate/20150413192223_add_public_email_to_users.rb5
-rw-r--r--db/migrate/20150417121913_create_project_import_data.rb8
-rw-r--r--db/migrate/20150417122318_remove_import_data_from_project.rb5
-rw-r--r--db/schema.rb19
-rw-r--r--doc/api/projects.md15
-rw-r--r--doc/customization/issue_closing.md35
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/integration/ldap.md5
-rw-r--r--doc/markdown/markdown.md4
-rw-r--r--doc/public_access/public_access.md2
-rw-r--r--doc/raketasks/backup_restore.md7
-rw-r--r--doc/release/patch.md11
-rw-r--r--doc/ssh/README.md8
-rw-r--r--doc/update/6.6-to-6.7.md3
-rw-r--r--doc/update/6.x-or-7.x-to-7.10.md (renamed from doc/update/6.x-or-7.x-to-7.9.md)26
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/forking/branch_select.pngbin0 -> 55352 bytes
-rw-r--r--doc/workflow/forking/fork_button.pngbin0 -> 68271 bytes
-rw-r--r--doc/workflow/forking/groups.pngbin0 -> 98109 bytes
-rw-r--r--doc/workflow/forking/merge_request.pngbin0 -> 60597 bytes
-rw-r--r--doc/workflow/forking_workflow.md36
-rw-r--r--docker/Dockerfile2
-rw-r--r--features/admin/deploy_keys.feature21
-rw-r--r--features/dashboard/issues.feature2
-rw-r--r--features/dashboard/merge_requests.feature2
-rw-r--r--features/groups.feature15
-rw-r--r--features/invites.feature45
-rw-r--r--features/project/deploy_keys.feature21
-rw-r--r--features/project/issues/filter_labels.feature6
-rw-r--r--features/project/issues/issues.feature7
-rw-r--r--features/project/star.feature2
-rw-r--r--features/project/team_management.feature6
-rw-r--r--features/project/wiki.feature24
-rw-r--r--features/steps/admin/deploy_keys.rb57
-rw-r--r--features/steps/dashboard/issues.rb17
-rw-r--r--features/steps/dashboard/merge_requests.rb17
-rw-r--r--features/steps/groups.rb43
-rw-r--r--features/steps/invites.rb80
-rw-r--r--features/steps/project/deploy_keys.rb26
-rw-r--r--features/steps/project/issues/filter_labels.rb23
-rw-r--r--features/steps/project/issues/issues.rb18
-rw-r--r--features/steps/project/merge_requests.rb3
-rw-r--r--features/steps/project/source/browse_files.rb2
-rw-r--r--features/steps/project/star.rb8
-rw-r--r--features/steps/project/team_management.rb16
-rw-r--r--features/steps/project/wiki.rb36
-rw-r--r--features/steps/shared/project.rb7
-rw-r--r--lib/api/branches.rb6
-rw-r--r--lib/api/entities.rb3
-rw-r--r--lib/api/group_members.rb11
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/projects.rb7
-rw-r--r--lib/api/repositories.rb11
-rw-r--r--lib/backup/manager.rb42
-rw-r--r--lib/file_size_validator.rb12
-rw-r--r--lib/gitlab.rb5
-rw-r--r--lib/gitlab/backend/shell.rb2
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb19
-rw-r--r--lib/gitlab/closing_issue_extractor.rb23
-rw-r--r--lib/gitlab/contributor.rb (renamed from lib/gitlab/contributors.rb)0
-rw-r--r--lib/gitlab/current_settings.rb3
-rw-r--r--lib/gitlab/github_import/project_creator.rb19
-rw-r--r--lib/gitlab/gitlab_import/client.rb4
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb19
-rw-r--r--lib/gitlab/gitorious_import/client.rb32
-rw-r--r--lib/gitlab/gitorious_import/project_creator.rb19
-rw-r--r--lib/gitlab/gitorious_import/repository.rb37
-rw-r--r--lib/gitlab/google_code_import/client.rb48
-rw-r--r--lib/gitlab/google_code_import/importer.rb360
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb37
-rw-r--r--lib/gitlab/google_code_import/repository.rb43
-rw-r--r--lib/gitlab/key_fingerprint.rb55
-rw-r--r--lib/gitlab/ldap/config.rb6
-rw-r--r--lib/gitlab/ldap/user.rb15
-rw-r--r--lib/gitlab/markdown.rb104
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb (renamed from lib/gitlab/oauth/auth_hash.rb)0
-rw-r--r--lib/gitlab/o_auth/user.rb (renamed from lib/gitlab/oauth/user.rb)2
-rw-r--r--lib/gitlab/reference_extractor.rb108
-rw-r--r--lib/gitlab/regex.rb66
-rw-r--r--lib/gitlab/satellite/satellite.rb5
-rw-r--r--lib/tasks/gitlab/backup.rake33
-rw-r--r--lib/tasks/gitlab/check.rake17
-rw-r--r--lib/tasks/gitlab/test.rake1
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb51
-rw-r--r--spec/controllers/help_controller_spec.rb61
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb113
-rw-r--r--spec/controllers/import/github_controller_spec.rb96
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb106
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb47
-rw-r--r--spec/controllers/namespaces_controller_spec.rb121
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb41
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb65
-rw-r--r--spec/factories.rb11
-rw-r--r--spec/factories_spec.rb6
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/issues_spec.rb4
-rw-r--r--spec/features/users_spec.rb43
-rw-r--r--spec/fixtures/GoogleCodeProjectHosting.json407
-rw-r--r--spec/helpers/application_helper_spec.rb26
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb10
-rw-r--r--spec/helpers/icons_helper_spec.rb109
-rw-r--r--spec/helpers/submodule_helper_spec.rb35
-rw-r--r--spec/lib/file_size_validator_spec.rb43
-rw-r--r--spec/lib/gitlab/bitbucket_import/project_creator_spec.rb6
-rw-r--r--spec/lib/gitlab/closing_issue_extractor_spec.rb62
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb6
-rw-r--r--spec/lib/gitlab/gitlab_import/project_creator_spec.rb6
-rw-r--r--spec/lib/gitlab/gitorious_import/project_creator_spec.rb (renamed from spec/lib/gitlab/gitorious_import/project_creator.rb)9
-rw-r--r--spec/lib/gitlab/google_code_import/client_spec.rb34
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb84
-rw-r--r--spec/lib/gitlab/google_code_import/project_creator_spec.rb27
-rw-r--r--spec/lib/gitlab/key_fingerprint_spec.rb12
-rw-r--r--spec/lib/gitlab/ldap/config_spec.rb14
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb67
-rw-r--r--spec/lib/gitlab/o_auth/auth_hash_spec.rb (renamed from spec/lib/gitlab/oauth/auth_hash_spec.rb)0
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb (renamed from spec/lib/gitlab/oauth/user_spec.rb)0
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb163
-rw-r--r--spec/lib/gitlab/regex_spec.rb16
-rw-r--r--spec/mailers/notify_spec.rb105
-rw-r--r--spec/models/commit_spec.rb2
-rw-r--r--spec/models/deploy_keys_project_spec.rb27
-rw-r--r--spec/models/key_spec.rb13
-rw-r--r--spec/models/member_spec.rb148
-rw-r--r--spec/models/members_spec.rb20
-rw-r--r--spec/models/namespace_spec.rb12
-rw-r--r--spec/models/project_services/asana_service_spec.rb (renamed from spec/models/asana_service_spec.rb)0
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb (renamed from spec/models/project_services/buildbox_service_spec.rb)4
-rw-r--r--spec/models/project_services/gitlab_ci_service_spec.rb21
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb17
-rw-r--r--spec/models/user_spec.rb10
-rw-r--r--spec/requests/api/projects_spec.rb13
-rw-r--r--spec/requests/api/repositories_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb4
-rw-r--r--spec/routing/project_routing_spec.rb3
-rw-r--r--spec/routing/routing_spec.rb53
-rw-r--r--spec/services/archive_repository_service_spec.rb93
-rw-r--r--spec/services/projects/fork_service_spec.rb14
-rw-r--r--spec/services/projects/upload_service_spec.rb10
-rw-r--r--spec/spec_helper.rb2
-rw-r--r--spec/support/select2_helper.rb4
-rw-r--r--spec/support/test_env.rb8
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb53
-rw-r--r--spec/workers/fork_registration_worker_spec.rb10
-rw-r--r--spec/workers/repository_archive_worker_spec.rb80
-rw-r--r--vendor/assets/javascripts/chart-lib.min.js8
388 files changed, 6579 insertions, 2076 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 7290d627d24..03b78d68840 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -652,7 +652,7 @@ Style/SymbolProc:
Style/Tab:
Description: 'No hard tabs.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#spaces-indentation'
- Enabled: false
+ Enabled: true
Style/TrailingBlankLines:
Description: 'Checks trailing blank lines and final newline.'
diff --git a/.ruby-version b/.ruby-version
index cd57a8b95d6..399088bf465 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-2.1.5
+2.1.6
diff --git a/CHANGELOG b/CHANGELOG
index 242d2c773c6..b82392c4e2b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,29 +1,60 @@
Please view this file on the master branch, on stable branches it's out of date.
+v 7.11.0 (unreleased)
+ - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
+ - Ignore invalid lines in .gitmodules
+ -
+ -
+ -
+ -
+ -
+ -
+ -
+
v 7.10.0 (unreleased)
+ - Allow projects to be imported from Google Code.
+ - Allow users to be invited by email to join a group or project.
+ - Don't crash when project repository doesn't exist.
+ - Add config var to block auto-created LDAP users.
+ - Don't use HTML ellipsis in EmailsOnPush subject truncated commit message.
+ - Set EmailsOnPush reply-to address to committer email when enabled.
+ - Fix broken file browsing with a submodule that contains a relative link (Stan Hu)
+ - Fix persistent XSS vulnerability around profile website URLs.
+ - Fix project import URL regex to prevent arbitary local repos from being imported.
+ - Fix directory traversal vulnerability around uploads routes.
+ - Fix directory traversal vulnerability around help pages.
+ - Don't leak existence of project via search autocomplete.
+ - Don't leak existence of group or project via search.
+ - Fix bug where Wiki pages that included a '/' were no longer accessible (Stan Hu)
+ - Fix bug where error messages from Dropzone would not be displayed on the issues page (Stan Hu)
+ - Add a rake task to check repository integrity with `git fsck`
+ - Add ability to configure Reply-To address in gitlab.yml (Stan Hu)
+ - Move current user to the top of the list in assignee/author filters (Stan Hu)
- Fix broken side-by-side diff view on merge request page (Stan Hu)
- Set Application controller default URL options to ensure all url_for calls are consistent (Stan Hu)
- Allow HTML tags in Markdown input
- Fix code unfold not working on Compare commits page (Stan Hu)
+ - Fix generating SSH key fingerprints with OpenSSH 6.8. (Sašo Stanovnik)
- Include missing events and fix save functionality in admin service template settings form (Stan Hu)
- Fix "Import projects from" button to show the correct instructions (Stan Hu)
- Fix dots in Wiki slugs causing errors (Stan Hu)
- - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
+ - Make maximum attachment size configurable via Application Settings (Stan Hu)
- Update poltergeist to version 1.6.0 to support PhantomJS 2.0 (Zeger-Jan van de Weg)
- Fix cross references when usernames, milestones, or project names contain underscores (Stan Hu)
- Disable reference creation for comments surrounded by code/preformatted blocks (Stan Hu)
- Reduce Rack Attack false positives causing 403 errors during HTTP authentication (Stan Hu)
- enable line wrapping per default and remove the checkbox to toggle it (Hannes Rosenögger)
- - extend the commit calendar to show the actual commits made on a date (Hannes Rosenögger)
- Fix a link in the patch update guide
- Add a service to support external wikis (Hannes Rosenögger)
+ - Omit the "email patches" link and fix plain diff view for merge commits
- List new commits for newly pushed branch in activity view.
- Add sidetiq gem dependency to match EE
- - Add changelog, license and contribution guide links to project sidebar.
+ - Add changelog, license and contribution guide links to project tab bar.
- Improve diff UI
- Fix alignment of navbar toggle button (Cody Mize)
- Fix checkbox rendering for nested task lists
- Identical look of selectboxes in UI
+ - Upgrade the gitlab_git gem to version 7.1.3
- Move "Import existing repository by URL" option to button.
- Improve error message when save profile has error.
- Passing the name of pushed ref to CI service (requires GitLab CI 7.9+)
@@ -37,18 +68,69 @@ v 7.10.0 (unreleased)
- Add ability to unlink connected accounts
- Replace commits calendar with faster contribution calendar that includes issues and merge requests
- Add inifinite scroll to user page activity
- - Don't show commit comment button when user is not signed in.
- Don't include system notes in issue/MR comment count.
- Don't mark merge request as updated when merge status relative to target branch changes.
- Link note avatar to user.
- Make Git-over-SSH errors more descriptive.
- Fix EmailsOnPush.
-
-v 7.9.0
- - Send EmailsOnPush email when branch or tag is created or deleted.
- - Faster merge request processing for large repository
- - Prevent doubling AJAX request with each commit visit via Turbolink
- - Prevent unnecessary doubling of js events on import pages and user calendar
+ - Refactor issue filtering
+ - AJAX selectbox for issue assignee and author filters
+ - Fix issue with missing options in issue filtering dropdown if selected one
+ - Prevent holding Control-Enter or Command-Enter from posting comment multiple times.
+ - Prevent note form from being cleared when submitting failed.
+ - Improve file icons rendering on tree (Sullivan Sénéchal)
+ - API: Add pagination to project events
+ - Get issue links in notification mail to work again.
+ - Don't show commit comment button when user is not signed in.
+ - Fix admin user projects lists.
+ - Don't leak private group existence by redirecting from namespace controller to group controller.
+ - Ability to skip some items from backup (database, respositories or uploads)
+ - Fix "Hello @username." references not working by no longer allowing usernames to end in period.
+ - Archive repositories in background worker.
+ - Import GitHub, Bitbucket or GitLab.com projects owned by authenticated user into current namespace.
+ - Project labels are now available over the API under the "tag_list" field (Cristian Medina)
+ - Fixed link paths for HTTP and SSH on the admin project view (Jeremy Maziarz)
+ - Fix and improve help rendering (Sullivan Sénéchal)
+ - Fix final line in EmailsOnPush email diff being rendered as error.
+ - Authometic setup GitLab CI project for forks if origin project has GitLab CI enabled
+ - Prevent duplicate Buildkite service creation.
+ - Fix git over ssh errors 'fatal: protocol error: bad line length character'
+ - Automatically setup GitLab CI project for forks if origin project has GitLab CI enabled
+ - Bust group page project list cache when namespace name or path changes.
+ - Explicitly set image alt-attribute to prevent graphical glitches if gravatars could not be loaded
+ - Allow user to choose a public email to show on public profile
+ - Remove truncation from issue titles on milestone page (Jason Blanchard)
+ - Fix stuck Merge Request merging events from old installations (Ben Bodenmiller)
+ - Fix merge request comments on files with multiple commits
+ - Fix Resource Owner Password Authentication Flow
+
+v 7.9.4
+ - Security: Fix project import URL regex to prevent arbitary local repos from being imported
+ - Fixed issue where only 25 commits would load in file listings
+ - Fix LDAP identities after config update
+
+v 7.9.3
+ - Contains no changes
+ - Add icons to Add dropdown items.
+ - Allow admin to create public deploy keys that are accessible to any project.
+ - Warn when gitlab-shell version doesn't match requirement.
+ - Skip email confirmation when set by admin or via LDAP.
+ - Only allow users to reference groups, projects, issues, MRs, commits they have access to.
+
+v 7.9.3
+ - Contains no changes
+
+v 7.9.2
+ - Contains no changes
+
+v 7.9.1
+ - Include missing events and fix save functionality in admin service template settings form (Stan Hu)
+ - Fix "Import projects from" button to show the correct instructions (Stan Hu)
+ - Fix OAuth2 issue importing a new project from GitHub and GitLab (Stan Hu)
+ - Fix for LDAP with commas in DN
+ - Fix missing events and in admin Slack service template settings form (Stan Hu)
+ - Don't show commit comment button when user is not signed in.
+ - Downgrade gemnasium-gitlab-service gem
v 7.9.0
- Add HipChat integration documentation (Stan Hu)
@@ -129,6 +211,10 @@ v 7.9.0
- Fix invalid Atom feeds when using emoji, horizontal rules, or images (Christian Walther)
- Backup of repositories with tar instead of git bundle (only now are git-annex files included in the backup)
- Add canceled status for CI
+ - Send EmailsOnPush email when branch or tag is created or deleted.
+ - Faster merge request processing for large repository
+ - Prevent doubling AJAX request with each commit visit via Turbolink
+ - Prevent unnecessary doubling of js events on import pages and user calendar
v 7.8.4
- Fix issue_tracker_id substitution in custom issue trackers
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 42b5ce22e32..3165b7379d3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -71,7 +71,7 @@ If you can, please submit a merge request with the fix or improvements including
1. Fork the project on GitLab Cloud
1. Create a feature branch
-1. Write [tests](README.md#run-the-tests) and code
+1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
1. Add your changes to the [CHANGELOG](CHANGELOG)
1. If you are changing the README, some documentation or other things which have no effect on the tests, add `[ci skip]` somewhere in the commit message
1. If you have multiple commits please combine them into one commit by [squashing them](http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index e70b4523ae7..097a15a2af3 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.0
+2.6.2
diff --git a/Gemfile b/Gemfile
index e7f75055f3f..3f262655117 100644
--- a/Gemfile
+++ b/Gemfile
@@ -39,7 +39,7 @@ gem "browser"
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.1.2'
+gem "gitlab_git", '~> 7.1.9'
# Ruby/Rack Git Smart-HTTP Server Handler
gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack'
@@ -94,7 +94,7 @@ gem 'html-pipeline-gitlab', '~> 0.1'
gem "github-markup"
# Required markup gems by github-markdown
-gem 'redcarpet', '~> 3.1.2'
+gem 'redcarpet', '~> 3.2.3'
gem 'RedCloth'
gem 'rdoc', '~>3.6'
gem 'org-ruby', '= 0.9.12'
@@ -115,7 +115,7 @@ end
gem "state_machine"
# Issue tags
-gem "acts-as-taggable-on"
+gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
gem 'slim'
@@ -208,7 +208,6 @@ group :development do
gem "letter_opener"
gem 'quiet_assets', '~> 1.0.1'
gem 'rack-mini-profiler', require: false
- gem "byebug"
# Better errors handler
gem 'better_errors'
@@ -257,6 +256,8 @@ group :development, :test do
gem "spring", '~> 1.3.1'
gem "spring-commands-rspec", '1.0.4'
gem "spring-commands-spinach", '1.0.0'
+
+ gem "byebug"
end
group :test do
diff --git a/Gemfile.lock b/Gemfile.lock
index 7da4d3c3583..bfe626521e7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -33,8 +33,8 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.1)
tzinfo (~> 1.1)
- acts-as-taggable-on (2.4.1)
- rails (>= 3, < 5)
+ acts-as-taggable-on (3.5.0)
+ activerecord (>= 3.2, < 5)
addressable (2.3.5)
annotate (2.6.0)
activerecord (>= 2.3.0)
@@ -188,7 +188,7 @@ GEM
dotenv (>= 0.7)
thor (>= 0.13.6)
formatador (0.2.4)
- gemnasium-gitlab-service (0.2.4)
+ gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (2.0.0)
json
@@ -212,7 +212,7 @@ GEM
mime-types (~> 1.19)
gitlab_emoji (0.1.0)
gemojione (~> 2.0)
- gitlab_git (7.1.2)
+ gitlab_git (7.1.9)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-linguist (~> 3.0)
@@ -457,7 +457,7 @@ GEM
ffi (>= 0.5.0)
rdoc (3.12.2)
json (~> 1.4)
- redcarpet (3.1.2)
+ redcarpet (3.2.3)
redis (3.1.0)
redis-actionpack (4.0.0)
actionpack (~> 4)
@@ -662,7 +662,7 @@ PLATFORMS
DEPENDENCIES
RedCloth
ace-rails-ap
- acts-as-taggable-on
+ acts-as-taggable-on (~> 3.4)
addressable
annotate (~> 2.6.0.beta2)
asana (~> 0.0.6)
@@ -703,7 +703,7 @@ DEPENDENCIES
gitlab-grack (~> 2.0.0.rc2)
gitlab-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
- gitlab_git (~> 7.1.2)
+ gitlab_git (~> 7.1.9)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (= 1.2.1)
gollum-lib (~> 4.0.2)
@@ -755,7 +755,7 @@ DEPENDENCIES
rb-fsevent
rb-inotify
rdoc (~> 3.6)
- redcarpet (~> 3.1.2)
+ redcarpet (~> 3.2.3)
redis-rails
request_store
rspec-rails (= 2.99)
diff --git a/LICENSE b/LICENSE
index d11b8730bf1..d8cb29f3638 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2014 GitLab B.V.
+Copyright (c) 2011-2015 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 a0ab4a734a4..799b92729fa 100644
--- a/Procfile
+++ b/Procfile
@@ -1,2 +1,2 @@
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
-worker: bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default
+worker: bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 27d04e7cac6..9e5d594c861 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -1,57 +1,7 @@
@Api =
groups_path: "/api/:version/groups.json"
group_path: "/api/:version/groups/:id.json"
- users_path: "/api/:version/users.json"
- user_path: "/api/:version/users/:id.json"
- notes_path: "/api/:version/projects/:id/notes.json"
namespaces_path: "/api/:version/namespaces.json"
- project_users_path: "/api/:version/projects/:id/users.json"
-
- # Get 20 (depends on api) recent notes
- # and sort the ascending from oldest to newest
- notes: (project_id, callback) ->
- url = Api.buildUrl(Api.notes_path)
- url = url.replace(':id', project_id)
-
- $.ajax(
- url: url,
- data:
- private_token: gon.api_token
- gfm: true
- recent: true
- dataType: "json"
- ).done (notes) ->
- notes.sort (a, b) ->
- return a.id - b.id
- callback(notes)
-
- user: (user_id, callback) ->
- url = Api.buildUrl(Api.user_path)
- url = url.replace(':id', user_id)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- dataType: "json"
- ).done (user) ->
- callback(user)
-
- # Return users list. Filtered by query
- # Only active users retrieved
- users: (query, callback) ->
- url = Api.buildUrl(Api.users_path)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- search: query
- per_page: 20
- active: true
- dataType: "json"
- ).done (users) ->
- callback(users)
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -80,23 +30,6 @@
).done (groups) ->
callback(groups)
- # Return project users list. Filtered by query
- # Only active users retrieved
- projectUsers: (project_id, query, callback) ->
- url = Api.buildUrl(Api.project_users_path)
- url = url.replace(':id', project_id)
-
- $.ajax(
- url: url
- data:
- private_token: gon.api_token
- search: query
- per_page: 20
- active: true
- dataType: "json"
- ).done (users) ->
- callback(users)
-
# Return namespaces list. Filtered by query
namespaces: (query, callback) ->
url = Api.buildUrl(Api.namespaces_path)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index fda142293bc..bd52d3d4d70 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -115,8 +115,8 @@ if location.hash
window.addEventListener "hashchange", shiftWindow
$ ->
- # Click a .one_click_select field, select the contents
- $(".one_click_select").on 'click', -> $(@).select()
+ # Click a .js-select-on-focus field, select the contents
+ $(".js-select-on-focus").on "focusin", -> $(this).select()
$('.remove-row').bind 'ajax:success', ->
$(this).closest('li').fadeOut()
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 37b7ba2cc10..44d75bd694f 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -20,9 +20,9 @@ class @Calendar
position: "top"
legend: [
0
- 1
- 4
- 7
+ 10
+ 20
+ 30
]
legendCellPadding: 3
onClick: (date, count) ->
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 3535d8c2cfc..330ebac6f75 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -100,6 +100,8 @@ class Dispatcher
when 'users:show'
new User()
new Activities()
+ when 'admin:users:show'
+ new ProjectsList()
switch path.first()
when 'admin'
@@ -127,7 +129,7 @@ class Dispatcher
when 'show'
new ProjectShow()
when 'issues', 'merge_requests'
- new ProjectUsersSelect()
+ new UsersSelect()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 06e9f0001ae..fca2a290e2d 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -10,6 +10,7 @@ class @DropzoneInput
iconSpinner = "<i class=\"fa fa-spinner fa-spin div-dropzone-icon\"></i>"
btnAlert = "<button type=\"button\"" + alertAttr + ">&times;</button>"
project_uploads_path = window.project_uploads_path or null
+ max_file_size = gon.max_file_size or 10
form_textarea = $(form).find("textarea.markdown-area")
form_textarea.wrap "<div class=\"div-dropzone\"></div>"
@@ -76,7 +77,7 @@ class @DropzoneInput
dictDefaultMessage: ""
clickable: true
paramName: "file"
- maxFilesize: 10
+ maxFilesize: max_file_size
uploadMultiple: false
headers:
"X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
@@ -108,9 +109,10 @@ class @DropzoneInput
return
error: (temp, errorMessage) ->
- checkIfMsgExists = $(".error-alert").children().length
+ errorAlert = $(form).find('.error-alert')
+ checkIfMsgExists = errorAlert.children().length
if checkIfMsgExists is 0
- $(".error-alert").append divAlert
+ errorAlert.append divAlert
$(".div-dropzone-alert").append btnAlert + errorMessage
return
@@ -221,9 +223,10 @@ class @DropzoneInput
"display": "none"
showError = (message) ->
- checkIfMsgExists = $(".error-alert").children().length
+ errorAlert = $(form).find('.error-alert')
+ checkIfMsgExists = errorAlert.children().length
if checkIfMsgExists is 0
- $(".error-alert").append divAlert
+ errorAlert.append divAlert
$(".div-dropzone-alert").append btnAlert + message
closeAlertMessage = ->
@@ -237,4 +240,4 @@ class @DropzoneInput
formatLink: (link) ->
text = "[#{link.alt}](#{link.url})"
text = "!#{text}" if link.is_image
- text \ No newline at end of file
+ text
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index bf71c144eaf..4e2e6550eb2 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -9,12 +9,8 @@ class @Issue
if $("a.btn-close").length
$("li.task-list-item input:checkbox").prop("disabled", false)
- $(".task-list-item input:checkbox").on(
- "click"
- null
- "issue"
- updateTaskState
- )
+ $('.task-list-item input:checkbox').off('change')
+ $('.task-list-item input:checkbox').change('issue', updateTaskState)
$('.issue-details').waitForImages ->
$('.issuable-affix').affix offset:
@@ -22,3 +18,7 @@ class @Issue
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
+ $('.issuable-affix').on 'affix.bs.affix', ->
+ $(@).width($(@).outerWidth())
+ .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
+ $(@).width('')
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 6127d2bb480..ae5d088d593 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -26,6 +26,10 @@ class @MergeRequest
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
+ $('.issuable-affix').on 'affix.bs.affix', ->
+ $(@).width($(@).outerWidth())
+ .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
+ $(@).width('')
# Local jQuery finder
$: (selector) ->
@@ -54,14 +58,6 @@ class @MergeRequest
, 'json'
bindEvents: ->
- this.$('.merge-request-tabs').on 'click', 'a', (event) =>
- a = $(event.currentTarget)
-
- href = a.attr('href')
- History.replaceState {path: href}, document.title, href
-
- event.preventDefault()
-
this.$('.merge-request-tabs').on 'click', 'li', (event) =>
this.activateTab($(event.currentTarget).data('action'))
@@ -81,12 +77,8 @@ class @MergeRequest
this.$('.remove_source_branch_in_progress').hide()
this.$('.remove_source_branch_widget.failed').show()
- $(".task-list-item input:checkbox").on(
- "click"
- null
- "merge_request"
- updateTaskState
- )
+ $('.task-list-item input:checkbox').off('change')
+ $('.task-list-item input:checkbox').change('merge_request', updateTaskState)
activateTab: (action) ->
this.$('.merge-request-tabs li').removeClass 'active'
@@ -164,4 +156,3 @@ class @MergeRequest
else
setTimeout(merge_request.mergeInProgress, 3000)
dataType: 'json'
-
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index c366c98cf54..6dfe10f0006 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -37,7 +37,8 @@ class @Notes
$(document).on "click", ".js-note-attachment-delete", @removeAttachment
# reset main target form after submit
- $(document).on "ajax:complete", ".js-main-target-form", @resetMainTargetForm
+ $(document).on "ajax:complete", ".js-main-target-form", @reenableTargetFormSubmitButton
+ $(document).on "ajax:success", ".js-main-target-form", @resetMainTargetForm
# update the file name when an attachment is selected
$(document).on "change", ".js-note-attachment-input", @updateFormAttachment
@@ -57,6 +58,7 @@ class @Notes
@notes_forms = '.js-main-target-form textarea, .js-discussion-note-form textarea'
# Chrome doesn't fire keypress or keyup for Command+Enter, so we need keydown.
$(document).on('keydown', @notes_forms, (e) ->
+ return if e.originalEvent.repeat
if e.keyCode == 10 || ((e.metaKey || e.ctrlKey) && e.keyCode == 13)
$(@).parents('form').submit()
)
@@ -70,6 +72,7 @@ class @Notes
$(document).off "click", ".js-note-delete"
$(document).off "click", ".js-note-attachment-delete"
$(document).off "ajax:complete", ".js-main-target-form"
+ $(document).off "ajax:success", ".js-main-target-form"
$(document).off "click", ".js-discussion-reply-button"
$(document).off "click", ".js-add-diff-note-button"
$(document).off "visibilitychange"
@@ -169,6 +172,11 @@ class @Notes
form.find(".js-note-text").data("autosave").reset()
+ reenableTargetFormSubmitButton: ->
+ form = $(".js-main-target-form")
+
+ form.find(".js-note-text").trigger "input"
+
###
Shows the main form and does some setup on it.
diff --git a/app/assets/javascripts/project_users_select.js.coffee b/app/assets/javascripts/project_users_select.js.coffee
deleted file mode 100644
index 80ab1a61ab9..00000000000
--- a/app/assets/javascripts/project_users_select.js.coffee
+++ /dev/null
@@ -1,56 +0,0 @@
-class @ProjectUsersSelect
- constructor: ->
- $('.ajax-project-users-select').each (i, select) =>
- project_id = $(select).data('project-id') || $('body').data('project-id')
-
- $(select).select2
- placeholder: $(select).data('placeholder') || "Search for a user"
- multiple: $(select).hasClass('multiselect')
- minimumInputLength: 0
- query: (query) ->
- Api.projectUsers project_id, query.term, (users) ->
- data = { results: users }
-
- if query.term.length == 0
- nullUser = {
- name: 'Unassigned',
- avatar: null,
- username: 'none',
- id: -1
- }
-
- data.results.unshift(nullUser)
-
- query.callback(data)
-
- initSelection: (element, callback) ->
- id = $(element).val()
- if id != "" && id != "-1"
- Api.user(id, callback)
-
-
- formatResult: (args...) =>
- @formatResult(args...)
- formatSelection: (args...) =>
- @formatSelection(args...)
- dropdownCssClass: "ajax-project-users-dropdown"
- dropdownAutoWidth: true
- escapeMarkup: (m) -> # we do not want to escape markup since we are displaying html in results
- m
-
- formatResult: (user) ->
- if user.avatar_url
- avatar = user.avatar_url
- else
- avatar = gon.default_avatar_url
-
- avatarMarkup = "<div class='user-image'><img class='avatar s24' src='#{avatar}'></div>"
-
- "<div class='user-result'>
- #{avatarMarkup}
- <div class='user-name'>#{user.name}</div>
- <div class='user-username'>#{user.username}</div>
- </div>"
-
- formatSelection: (user) ->
- user.name
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
index d522d9f3b90..4a05bdccdb3 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee
@@ -3,12 +3,12 @@
class @ShortcutsDashboardNavigation extends Shortcuts
constructor: ->
super()
- Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-activity'))
- Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-projects'))
- Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-issues'))
- Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndollowLink('.shortcuts-merge_requests'))
+ Mousetrap.bind('g a', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-activity'))
+ Mousetrap.bind('g i', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-issues'))
+ Mousetrap.bind('g m', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-merge_requests'))
+ Mousetrap.bind('g p', -> ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-projects'))
- @findAndollowLink: (selector) ->
+ @findAndFollowLink: (selector) ->
link = $(selector).attr('href')
if link
window.location = link
diff --git a/app/assets/javascripts/shortcuts_navigation.coffee b/app/assets/javascripts/shortcuts_navigation.coffee
index e592b700e7c..31895fbf2bc 100644
--- a/app/assets/javascripts/shortcuts_navigation.coffee
+++ b/app/assets/javascripts/shortcuts_navigation.coffee
@@ -3,18 +3,18 @@
class @ShortcutsNavigation extends Shortcuts
constructor: ->
super()
- Mousetrap.bind('g p', -> ShortcutsNavigation.findAndollowLink('.shortcuts-project'))
- Mousetrap.bind('g f', -> ShortcutsNavigation.findAndollowLink('.shortcuts-tree'))
- Mousetrap.bind('g c', -> ShortcutsNavigation.findAndollowLink('.shortcuts-commits'))
- Mousetrap.bind('g n', -> ShortcutsNavigation.findAndollowLink('.shortcuts-network'))
- Mousetrap.bind('g g', -> ShortcutsNavigation.findAndollowLink('.shortcuts-graphs'))
- Mousetrap.bind('g i', -> ShortcutsNavigation.findAndollowLink('.shortcuts-issues'))
- Mousetrap.bind('g m', -> ShortcutsNavigation.findAndollowLink('.shortcuts-merge_requests'))
- Mousetrap.bind('g w', -> ShortcutsNavigation.findAndollowLink('.shortcuts-wiki'))
- Mousetrap.bind('g s', -> ShortcutsNavigation.findAndollowLink('.shortcuts-snippets'))
+ Mousetrap.bind('g p', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-project'))
+ Mousetrap.bind('g f', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'))
+ Mousetrap.bind('g c', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'))
+ Mousetrap.bind('g n', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-network'))
+ Mousetrap.bind('g g', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-graphs'))
+ Mousetrap.bind('g i', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-issues'))
+ Mousetrap.bind('g m', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests'))
+ Mousetrap.bind('g w', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki'))
+ Mousetrap.bind('g s', -> ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets'))
@enabledHelp.push('.hidden-shortcut.project')
-
- @findAndollowLink: (selector) ->
+
+ @findAndFollowLink: (selector) ->
link = $(selector).attr('href')
if link
window.location = link
diff --git a/app/assets/javascripts/sidebar.js.coffee b/app/assets/javascripts/sidebar.js.coffee
index 7febcba0e94..2e3f5608257 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -3,12 +3,7 @@ $(document).on("click", '.toggle-nav-collapse', (e) ->
collapsed = 'page-sidebar-collapsed'
expanded = 'page-sidebar-expanded'
- if $('.page-with-sidebar').hasClass(collapsed)
- $('.page-with-sidebar').removeClass(collapsed).addClass(expanded)
- $('.toggle-nav-collapse i').removeClass('fa-angle-right').addClass('fa-angle-left')
- $.cookie("collapsed_nav", "false", { path: '/' })
- else
- $('.page-with-sidebar').removeClass(expanded).addClass(collapsed)
- $('.toggle-nav-collapse i').removeClass('fa-angle-left').addClass('fa-angle-right')
- $.cookie("collapsed_nav", "true", { path: '/' })
+ $('.page-with-sidebar').toggleClass("#{collapsed} #{expanded}")
+ $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
+ $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
)
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index 9eee7406511..aeeed9ca3cc 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -1,20 +1,66 @@
class @UsersSelect
constructor: ->
+ @usersPath = "/autocomplete/users.json"
+ @userPath = "/autocomplete/users/:id.json"
+
$('.ajax-users-select').each (i, select) =>
+ @projectId = $(select).data('project-id')
+ @groupId = $(select).data('group-id')
+ showNullUser = $(select).data('null-user')
+ showAnyUser = $(select).data('any-user')
+ showEmailUser = $(select).data('email-user')
+ firstUser = $(select).data('first-user')
+
$(select).select2
placeholder: "Search for a user"
multiple: $(select).hasClass('multiselect')
minimumInputLength: 0
- query: (query) ->
- Api.users query.term, (users) ->
+ query: (query) =>
+ @users query.term, (users) =>
data = { results: users }
+
+ if query.term.length == 0
+ if firstUser
+ # Move current user to the front of the list
+ for obj, index in data.results
+ if obj.username == firstUser
+ data.results.splice(index, 1)
+ data.results.unshift(obj)
+ break
+
+ if showNullUser
+ nullUser = {
+ name: 'Unassigned',
+ avatar: null,
+ username: 'none',
+ id: 0
+ }
+ data.results.unshift(nullUser)
+
+ if showAnyUser
+ anyUser = {
+ name: 'Any',
+ avatar: null,
+ username: 'none',
+ id: null
+ }
+ data.results.unshift(anyUser)
+
+ if showEmailUser && data.results.length == 0 && query.term.match(/^[^@]+@[^@]+$/)
+ emailUser = {
+ name: "Invite \"#{query.term}\"",
+ avatar: null,
+ username: query.term,
+ id: query.term
+ }
+ data.results.unshift(emailUser)
+
query.callback(data)
- initSelection: (element, callback) ->
+ initSelection: (element, callback) =>
id = $(element).val()
- if id isnt ""
- Api.user(id, callback)
-
+ if id != "" && id != "0"
+ @user(id, callback)
formatResult: (args...) =>
@formatResult(args...)
@@ -38,3 +84,34 @@ class @UsersSelect
formatSelection: (user) ->
user.name
+
+ user: (user_id, callback) =>
+ url = @buildUrl(@userPath)
+ url = url.replace(':id', user_id)
+
+ $.ajax(
+ url: url
+ dataType: "json"
+ ).done (user) ->
+ callback(user)
+
+ # Return users list. Filtered by query
+ # Only active users retrieved
+ users: (query, callback) =>
+ url = @buildUrl(@usersPath)
+
+ $.ajax(
+ url: url
+ data:
+ search: query
+ per_page: 20
+ active: true
+ project_id: @projectId
+ group_id: @groupId
+ dataType: "json"
+ ).done (users) ->
+ callback(users)
+
+ buildUrl: (url) ->
+ url = gon.relative_url_root + url if gon.relative_url_root?
+ return url
diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss
index ccba65e3fd5..216f25cdcd5 100644
--- a/app/assets/stylesheets/base/mixins.scss
+++ b/app/assets/stylesheets/base/mixins.scss
@@ -119,6 +119,22 @@
li {
line-height: 1.5;
}
+
+ a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] {
+ &:before {
+ margin-right: 4px;
+
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ content: "\f0c6";
+ }
+
+ &:hover:before {
+ text-decoration: none;
+ }
+ }
}
@mixin str-truncated($max_width: 82%) {
diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss
index db393e08819..7c3021989a8 100644
--- a/app/assets/stylesheets/generic/common.scss
+++ b/app/assets/stylesheets/generic/common.scss
@@ -246,7 +246,7 @@ li.note {
.milestone {
&.milestone-closed {
- background: #eee;
+ background: #f9f9f9;
}
.progress {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/generic/filters.scss
new file mode 100644
index 00000000000..bd93a79722d
--- /dev/null
+++ b/app/assets/stylesheets/generic/filters.scss
@@ -0,0 +1,55 @@
+.filter-item {
+ margin-right: 15px;
+}
+
+.issues-state-filters {
+ li.active a {
+ border-color: #DDD !important;
+
+ &, &:hover, &:active, &.active {
+ background: #f5f5f5 !important;
+ border-bottom: 1px solid #f5f5f5 !important;
+ }
+ }
+}
+
+.issues-details-filters {
+ font-size: 13px;
+ background: #f5f5f5;
+ margin: -10px 0;
+ padding: 10px 15px;
+ margin-top: -15px;
+ border-left: 1px solid #DDD;
+ border-right: 1px solid #DDD;
+
+ .btn {
+ font-size: 13px;
+ }
+}
+
+@media (min-width: 800px) {
+ .issues-filters,
+ .issues_bulk_update {
+ select, .select2-container {
+ width: 120px !important;
+ display: inline-block;
+ }
+ }
+}
+
+@media (min-width: 1200px) {
+ .issues-filters,
+ .issues_bulk_update {
+ select, .select2-container {
+ width: 150px !important;
+ display: inline-block;
+ }
+ }
+}
+
+.issues-filters,
+.issues_bulk_update {
+ .select2-container .select2-choice {
+ color: #444 !important;
+ }
+}
diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss
index 1b0e056216f..71a1fc4493f 100644
--- a/app/assets/stylesheets/generic/mobile.scss
+++ b/app/assets/stylesheets/generic/mobile.scss
@@ -24,13 +24,6 @@
display: none !important;
}
- .project-home-panel {
- .star-fork-buttons {
- padding-top: 10px;
- padding-right: 15px;
- }
- }
-
.project-home-links {
display: none;
}
diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss
index 7557f411111..d8e0dc028d1 100644
--- a/app/assets/stylesheets/generic/selects.scss
+++ b/app/assets/stylesheets/generic/selects.scss
@@ -2,20 +2,25 @@
.select2-container, .select2-container.select2-drop-above {
.select2-choice {
background: #FFF;
- border-color: #CCC;
+ border-color: #DDD;
+ height: 34px;
padding: 6px 14px;
+ font-size: 14px;
line-height: 1.42857143;
- height: auto;
+
+ @include border-radius(4px);
.select2-arrow {
background: #FFF;
- border-left: 1px solid #DDD;
+ border-left: none;
+ padding-top: 3px;
}
}
}
.select2-container-multi .select2-choices {
- @include border-radius(4px)
+ @include border-radius(4px);
+ border-color: #CCC;
}
.select2-container-multi .select2-choices .select2-search-field input {
@@ -28,6 +33,7 @@
.select2-drop-active {
border: 1px solid #BBB !important;
margin-top: 4px;
+ font-size: 13px;
&.select2-drop-above {
margin-bottom: 8px;
@@ -106,3 +112,7 @@
font-weight: bolder;
}
}
+
+.ajax-users-dropdown {
+ min-width: 225px !important;
+}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 83daea10ed5..d4af7506d5b 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -45,7 +45,8 @@
padding: 12px 0px;
border-bottom: 1px solid #eee;
.event-title {
- @include str-truncated(72%);
+ max-width: 70%;
+ @include str-truncated(calc(100% - 174px));
font-weight: 500;
font-size: 14px;
.author_name {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 6c1dd4f7e9f..cd86a9be8b2 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -41,12 +41,9 @@
}
.check-all-holder {
- height: 36px;
+ line-height: 36px;
float: left;
- margin-right: 12px;
- padding: 6px 15px;
- border: 1px solid #ccc;
- @include border-radius(4px);
+ margin-right: 15px;
}
.issues_content {
@@ -59,30 +56,6 @@
}
}
-@media (min-width: 800px) {
- .issues_bulk_update {
- select, .select2-container {
- width: 120px !important;
- display: inline-block;
- }
- }
-}
-
-@media (min-width: 1200px) {
- .issues_bulk_update {
- select, .select2-container {
- width: 160px !important;
- display: inline-block;
- }
- }
-}
-
-.issues_bulk_update {
- .select2-container .select2-choice {
- color: #444 !important;
- }
-}
-
.participants {
margin-bottom: 20px;
}
@@ -120,12 +93,12 @@ form.edit-issue {
}
&.closed {
- background: #F5f5f5;
+ background: #F9F9F9;
border-color: #E5E5E5;
}
&.merged {
- background: #F5f5f5;
+ background: #F9F9F9;
border-color: #E5E5E5;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index d66093bc2e5..facd7e19314 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -62,20 +62,8 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
- a[href*="/uploads/"] {
- &:before {
- margin-right: 4px;
-
- font: normal normal normal 14px/1 FontAwesome;
- font-size: inherit;
- text-rendering: auto;
- -webkit-font-smoothing: antialiased;
- content: "\f0c6";
- }
-
- &:hover:before {
- text-decoration: none;
- }
+ hr {
+ margin: 10px 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6d55a5fa66e..c005470355e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -15,7 +15,7 @@
}
.project-home-panel {
- margin-bottom: 15px;
+ margin-bottom: 20px;
position: relative;
padding-left: 85px;
@@ -38,6 +38,11 @@
font-size: 45px;
line-height: 1.6;
}
+
+ .avatar, .identicon {
+ @include border-radius(4px);
+ box-shadow: 0 1px 2px #ddd;
+ }
}
.project-home-dropdown {
@@ -49,44 +54,49 @@
@extend .clearfix;
margin-bottom: 15px;
- .project-home-desc,
- .star-fork-buttons {
+ &.project-home-row-top {
+ margin-bottom: 15px;
+ }
+
+ .project-home-desc {
font-size: 16px;
line-height: 1.3;
+ margin-right: 215px;
}
.project-home-desc {
float: left;
- color: #666;
- }
-
- .star-fork-buttons {
- float: right;
- min-width: 200px;
- font-weight: bold;
-
- .star-buttons, .fork-buttons {
- float: right;
- margin-left: 20px;
-
- a:hover {
- text-decoration: none;
- }
-
- .count {
- margin-left: 5px;
- }
- }
+ color: $gray;
}
}
.visibility-level-label {
- color: #555;
- font-weight: bold;
+ color: $gray;
i {
color: inherit;
}
}
+
+ .project-repo-buttons {
+ margin-top: -3px;
+ position: absolute;
+ right: 0;
+ width: 260px;
+ text-align: right;
+
+ .btn {
+ font-weight: bold;
+ font-size: 14px;
+ line-height: 16px;
+
+ .count {
+ padding-left: 10px;
+ border-left: 1px solid #ccc;
+ display: inline-block;
+ margin-left: 10px;
+ }
+ }
+ }
}
.project-home-links {
@@ -109,6 +119,10 @@
background: #FAFAFA;
width: 100%;
}
+
+ .input-group-addon {
+ background: #FAFAFA;
+ }
}
.project-visibility-level-holder {
@@ -127,7 +141,7 @@
.option-descr {
margin-left: 24px;
- color: #666;
+ color: $gray;
}
}
}
@@ -206,8 +220,10 @@ ul.nav.nav-projects-tabs {
white-space: normal;
text-align: left;
padding: 10px 15px;
- background-color: #F9F9F9;
- border-color: #DDD;
+
+ &.dropdown-toggle {
+ text-align: center;
+ }
&:hover {
background-color: #eee;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9a5685877f8..b5fda196bf0 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -38,6 +38,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:twitter_sharing_enabled,
:sign_in_text,
:home_page_url,
+ :max_attachment_size,
restricted_visibility_levels: []
)
end
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
new file mode 100644
index 00000000000..e93603bef36
--- /dev/null
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -0,0 +1,49 @@
+class Admin::DeployKeysController < Admin::ApplicationController
+ before_filter :deploy_keys, only: [:index]
+ before_filter :deploy_key, only: [:show, :destroy]
+
+ def index
+
+ end
+
+ def show
+
+ end
+
+ def new
+ @deploy_key = deploy_keys.new
+ end
+
+ def create
+ @deploy_key = deploy_keys.new(deploy_key_params)
+
+ if @deploy_key.save
+ redirect_to admin_deploy_keys_path
+ else
+ render "new"
+ end
+ end
+
+ def destroy
+ deploy_key.destroy
+
+ respond_to do |format|
+ format.html { redirect_to admin_deploy_keys_path }
+ format.json { head :ok }
+ end
+ end
+
+ protected
+
+ def deploy_key
+ @deploy_key ||= deploy_keys.find(params[:id])
+ end
+
+ def deploy_keys
+ @deploy_keys ||= DeployKey.are_public
+ end
+
+ def deploy_key_params
+ params.require(:deploy_key).permit(:key, :title)
+ end
+end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 9d9adaa467f..22d045fc388 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -41,7 +41,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level])
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 693970e5349..b4c011f213c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -72,8 +72,8 @@ class Admin::UsersController < Admin::ApplicationController
end
respond_to do |format|
+ user.skip_reconfirmation!
if user.update_attributes(user_params_with_pass)
- user.confirm!
format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' }
format.json { head :ok }
else
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2809f90c0d5..920a981e7c9 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -126,7 +126,7 @@ class ApplicationController < ActionController::Base
def repository
@repository ||= project.repository
- rescue Grit::NoSuchPathError(e)
+ rescue Grit::NoSuchPathError => e
log_exception(e)
nil
end
@@ -153,7 +153,7 @@ class ApplicationController < ActionController::Base
end
def method_missing(method_sym, *arguments, &block)
- if method_sym.to_s =~ /^authorize_(.*)!$/
+ if method_sym.to_s =~ /\Aauthorize_(.*)!\z/
authorize_project!($1.to_sym)
else
super
@@ -203,6 +203,7 @@ class ApplicationController < ActionController::Base
gon.api_version = API::API.version
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.default_avatar_url = URI::join(Gitlab.config.gitlab.url, ActionController::Base.helpers.image_path('no_avatar.png')).to_s
+ gon.max_file_size = current_application_settings.max_attachment_size;
if current_user
gon.current_user_id = current_user.id
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
new file mode 100644
index 00000000000..11af9895261
--- /dev/null
+++ b/app/controllers/autocomplete_controller.rb
@@ -0,0 +1,30 @@
+class AutocompleteController < ApplicationController
+ def users
+ @users =
+ if params[:project_id].present?
+ project = Project.find(params[:project_id])
+
+ if can?(current_user, :read_project, project)
+ project.team.users
+ end
+ elsif params[:group_id]
+ group = Group.find(params[:group_id])
+
+ if can?(current_user, :read_group, group)
+ group.users
+ end
+ else
+ User.all
+ end
+
+ @users = @users.search(params[:search]) if params[:search].present?
+ @users = @users.active
+ @users = @users.page(params[:page]).per(PER_PAGE)
+ render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
+ end
+
+ def user
+ @user = User.find(params[:id])
+ render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
+ end
+end
diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb
index bc98eab133c..af1faca93f6 100644
--- a/app/controllers/confirmations_controller.rb
+++ b/app/controllers/confirmations_controller.rb
@@ -4,11 +4,11 @@ class ConfirmationsController < Devise::ConfirmationsController
def after_confirmation_path_for(resource_name, resource)
if signed_in?(resource_name)
- signed_in_root_path(resource)
+ after_sign_in_path_for(resource)
else
sign_in(resource)
if signed_in?(resource_name)
- signed_in_root_path(resource)
+ after_sign_in_path_for(resource)
else
new_session_path(resource_name)
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index a73b8fa212a..469a6813ee2 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -13,7 +13,7 @@ class Groups::ApplicationController < ApplicationController
end
def authorize_admin_group!
- unless can?(current_user, :manage_group, group)
+ unless can?(current_user, :admin_group, group)
return render_404
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 2df51c97a22..265cf4f0f4a 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -11,6 +11,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index
@project = @group.projects.find(params[:project_id]) if params[:project_id]
@members = @group.group_members
+ @members = @members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -22,7 +23,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level])
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -38,7 +39,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
if can?(current_user, :destroy_group_member, @group_member) # May fail if last owner.
@group_member.destroy
respond_to do |format|
- format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
+ format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
format.js { render nothing: true }
end
else
@@ -46,12 +47,26 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
end
+ def resend_invite
+ redirect_path = group_group_members_path(@group)
+
+ @group_member = @group.group_members.find(params[:id])
+
+ if @group_member.invite?
+ @group_member.resend_invite
+
+ redirect_to redirect_path, notice: 'The invitation was successfully resent.'
+ else
+ redirect_to redirect_path, alert: 'The invitation has already been accepted.'
+ end
+ end
+
def leave
@group_member = @group.group_members.where(user_id: current_user.id).first
if can?(current_user, :destroy_group_member, @group_member)
@group_member.destroy
- redirect_to(dashboard_groups_path, info: "You left #{group.name} group.")
+ redirect_to(dashboard_groups_path, notice: "You left #{group.name} group.")
else
return render_403
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index c46b8fff88f..546ff2cc71f 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -51,6 +51,6 @@ class Groups::MilestonesController < ApplicationController
end
def authorize_group_milestone!
- return render_404 unless can?(current_user, :manage_group, group)
+ return render_404 unless can?(current_user, :admin_group, group)
end
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index c4d620d87b1..35ece5b270b 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -3,13 +3,36 @@ class HelpController < ApplicationController
end
def show
- @category = params[:category]
- @file = params[:file]
+ category = clean_path_info(path_params[:category])
+ file = path_params[:file]
- if File.exists?(Rails.root.join('doc', @category, @file + '.md'))
- render 'show'
- else
- not_found!
+ respond_to do |format|
+ format.any(:markdown, :md, :html) do
+ path = Rails.root.join('doc', category, "#{file}.md")
+
+ if File.exist?(path)
+ @markdown = File.read(path)
+
+ render 'show.html.haml'
+ else
+ # Force template to Haml
+ render 'errors/not_found.html.haml', layout: 'errors', status: 404
+ end
+ end
+
+ # Allow access to images in the doc folder
+ format.any(:png, :gif, :jpeg) do
+ path = Rails.root.join('doc', category, "#{file}.#{params[:format]}")
+
+ if File.exist?(path)
+ send_file(path, disposition: 'inline')
+ else
+ head :not_found
+ end
+ end
+
+ # Any other format we don't recognize, just respond 404
+ format.any { head :not_found }
end
end
@@ -18,4 +41,44 @@ class HelpController < ApplicationController
def ui
end
+
+ private
+
+ def path_params
+ params.require(:category)
+ params.require(:file)
+
+ params
+ end
+
+ PATH_SEPS = Regexp.union(*[::File::SEPARATOR, ::File::ALT_SEPARATOR].compact)
+
+ # Taken from ActionDispatch::FileHandler
+ # Cleans up the path, to prevent directory traversal outside the doc folder.
+ def clean_path_info(path_info)
+ parts = path_info.split(PATH_SEPS)
+
+ clean = []
+
+ # Walk over each part of the path
+ parts.each do |part|
+ # Turn `one//two` or `one/./two` into `one/two`.
+ next if part.empty? || part == '.'
+
+ if part == '..'
+ # Turn `one/two/../` into `one`
+ clean.pop
+ else
+ # Add simple folder names to the clean path.
+ clean << part
+ end
+ end
+
+ # If the path was an absolute path (i.e. `/` or `/one/two`),
+ # add `/` to the front of the clean path.
+ clean.unshift '/' if parts.empty? || parts.first.empty?
+
+ # Join all the clean path parts by the path separator.
+ ::File.join(*clean)
+ end
end
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index edb8bd4160b..93a7ace3530 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -8,7 +8,7 @@ class Import::BaseController < ApplicationController
namespace.add_owner(current_user)
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
namespace = Namespace.find_by_path_or_name(@target_namespace)
- unless namespace.owner == current_user
+ unless current_user.can?(:create_projects, namespace)
@already_been_taken = true
return false
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 83ebc5fddca..bb8d7e0235c 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -36,8 +36,11 @@ class Import::BitbucketController < Import::BaseController
def create
@repo_id = params[:repo_id] || ""
repo = client.project(@repo_id.gsub("___", "/"))
- @target_namespace = params[:new_namespace].presence || repo["owner"]
@project_name = repo["slug"]
+
+ repo_owner = repo["owner"]
+ repo_owner = current_user.username if repo_owner == client.user["user"]["username"]
+ @target_namespace = params[:new_namespace].presence || repo_owner
namespace = get_or_create_namespace || (render and return)
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 8650b6464dc..87b41454c77 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -31,8 +31,11 @@ class Import::GithubController < Import::BaseController
def create
@repo_id = params[:repo_id].to_i
repo = client.repo(@repo_id)
- @target_namespace = params[:new_namespace].presence || repo.owner.login
@project_name = repo.name
+
+ repo_owner = repo.owner.login
+ repo_owner = current_user.username if repo_owner == client.user.login
+ @target_namespace = params[:new_namespace].presence || repo_owner
namespace = get_or_create_namespace || (render and return)
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index e979dad4b11..bddbfded812 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -28,8 +28,11 @@ class Import::GitlabController < Import::BaseController
def create
@repo_id = params[:repo_id].to_i
repo = client.project(@repo_id)
- @target_namespace = params[:new_namespace].presence || repo["namespace"]["path"]
@project_name = repo["name"]
+
+ repo_owner = repo["namespace"]["path"]
+ repo_owner = current_user.username if repo_owner == client.user["username"]
+ @target_namespace = params[:new_namespace].presence || repo_owner
namespace = get_or_create_namespace || (render and return)
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
new file mode 100644
index 00000000000..73c912e285b
--- /dev/null
+++ b/app/controllers/import/google_code_controller.rb
@@ -0,0 +1,116 @@
+class Import::GoogleCodeController < Import::BaseController
+ before_filter :user_map, only: [:new_user_map, :create_user_map]
+
+ def new
+
+ end
+
+ def callback
+ dump_file = params[:dump_file]
+
+ unless dump_file.respond_to?(:read)
+ return redirect_to :back, alert: "You need to upload a Google Takeout archive."
+ end
+
+ begin
+ dump = JSON.parse(dump_file.read)
+ rescue
+ return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive."
+ end
+
+ client = Gitlab::GoogleCodeImport::Client.new(dump)
+ unless client.valid?
+ return redirect_to :back, alert: "The uploaded file is not a valid Google Takeout archive."
+ end
+
+ session[:google_code_dump] = dump
+
+ if params[:create_user_map] == "1"
+ redirect_to new_user_map_import_google_code_path
+ else
+ redirect_to status_import_google_code_path
+ end
+ end
+
+ def new_user_map
+
+ end
+
+ def create_user_map
+ user_map_json = params[:user_map]
+ user_map_json = "{}" if user_map_json.blank?
+
+ begin
+ user_map = JSON.parse(user_map_json)
+ rescue
+ flash.now[:alert] = "The entered user map is not a valid JSON user map."
+
+ render "new_user_map" and return
+ end
+
+ unless user_map.is_a?(Hash) && user_map.all? { |k, v| k.is_a?(String) && v.is_a?(String) }
+ flash.now[:alert] = "The entered user map is not a valid JSON user map."
+
+ render "new_user_map" and return
+ end
+
+ # This is the default, so let's not save it into the database.
+ user_map.reject! do |key, value|
+ value == Gitlab::GoogleCodeImport::Client.mask_email(key)
+ end
+
+ session[:google_code_user_map] = user_map
+
+ flash[:notice] = "The user map has been saved. Continue by selecting the projects you want to import."
+
+ redirect_to status_import_google_code_path
+ end
+
+ def status
+ unless client.valid?
+ return redirect_to new_import_google_path
+ end
+
+ @repos = client.repos
+
+ @already_added_projects = current_user.created_projects.where(import_type: "google_code")
+ already_added_projects_names = @already_added_projects.pluck(:import_source)
+
+ @repos.reject! { |repo| already_added_projects_names.include? repo.name }
+ end
+
+ def jobs
+ jobs = current_user.created_projects.where(import_type: "google_code").to_json(only: [:id, :import_status])
+ render json: jobs
+ end
+
+ def create
+ @repo_id = params[:repo_id]
+ repo = client.repo(@repo_id)
+ @target_namespace = current_user.namespace
+ @project_name = repo.name
+
+ namespace = @target_namespace
+
+ user_map = session[:google_code_user_map]
+
+ @project = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, current_user, user_map).execute
+ end
+
+ private
+
+ def client
+ @client ||= Gitlab::GoogleCodeImport::Client.new(session[:google_code_dump])
+ end
+
+ def user_map
+ @user_map ||= begin
+ user_map = client.user_map
+
+ stored_user_map = session[:google_code_user_map]
+ user_map.update(stored_user_map) if stored_user_map
+
+ Hash[user_map.sort]
+ end
+ end
+end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
new file mode 100644
index 00000000000..1f97ff16c55
--- /dev/null
+++ b/app/controllers/invites_controller.rb
@@ -0,0 +1,83 @@
+class InvitesController < ApplicationController
+ before_filter :member
+ skip_before_filter :authenticate_user!, only: :decline
+
+ respond_to :html
+
+ layout 'navless'
+
+ def show
+
+ end
+
+ def accept
+ if member.accept_invite!(current_user)
+ label, path = source_info(member.source)
+
+ redirect_to path, notice: "You have been granted #{member.human_access} access to #{label}."
+ else
+ redirect_to :back, alert: "The invitation could not be accepted."
+ end
+ end
+
+ def decline
+ if member.decline_invite!
+ label, _ = source_info(member.source)
+
+ path =
+ if current_user
+ dashboard_path
+ else
+ new_user_session_path
+ end
+
+ redirect_to path, notice: "You have declined the invitation to join #{label}."
+ else
+ redirect_to :back, alert: "The invitation could not be declined."
+ end
+ end
+
+ private
+
+ def member
+ return @member if defined?(@member)
+
+ @token = params[:id]
+ @member = Member.find_by_invite_token(@token)
+
+ unless @member
+ render_404 and return
+ end
+
+ @member
+ end
+
+ def authenticate_user!
+ return if current_user
+
+ notice = "To accept this invitation, sign in"
+ notice << " or create an account" if current_application_settings.signup_enabled?
+ notice << "."
+
+ store_location_for :user, request.fullpath
+ redirect_to new_user_session_path, notice: notice
+ end
+
+ def source_info(source)
+ case source
+ when Project
+ project = member.source
+ label = "project #{project.name_with_namespace}"
+ path = namespace_project_path(project.namespace, project)
+ when Group
+ group = member.source
+ label = "group #{group.name}"
+ path = group_path(group)
+ else
+ label = "who knows what"
+ path = dashboard_path
+ end
+
+ [label, path]
+ end
+end
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
index b7a9d8c1291..386d103ee5a 100644
--- a/app/controllers/namespaces_controller.rb
+++ b/app/controllers/namespaces_controller.rb
@@ -4,14 +4,22 @@ class NamespacesController < ApplicationController
def show
namespace = Namespace.find_by(path: params[:id])
- unless namespace
- return render_404
+ if namespace
+ if namespace.is_a?(Group)
+ group = namespace
+ else
+ user = namespace.owner
+ end
end
- if namespace.type == "Group"
- redirect_to group_path(namespace)
+ if user
+ redirect_to user_path(user)
+ elsif group && can?(current_user, :read_group, group)
+ redirect_to group_path(group)
+ elsif current_user.nil?
+ authenticate_user!
else
- redirect_to user_path(namespace.owner)
+ render_404
end
end
end
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 4a65c978e5c..954c98c0d9f 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -3,6 +3,7 @@ class Profiles::EmailsController < ApplicationController
def index
@primary = current_user.email
+ @public_email = current_user.public_email
@emails = current_user.emails
end
@@ -19,7 +20,8 @@ class Profiles::EmailsController < ApplicationController
@email.destroy
current_user.set_notification_email
- current_user.save if current_user.notification_email_changed?
+ current_user.set_public_email
+ current_user.save if current_user.notification_email_changed? or current_user.public_email_changed?
respond_to do |format|
format.html { redirect_to profile_emails_url }
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 9252e85e8cc..7f76906066d 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -67,9 +67,10 @@ class ProfilesController < ApplicationController
def user_params
params.require(:user).permit(
- :email, :password, :password_confirmation, :bio, :name, :username,
- :skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id,
- :avatar, :hide_no_ssh_key, :hide_no_password, :location
+ :email, :password, :password_confirmation, :bio, :name,
+ :username, :skype, :linkedin, :twitter, :website_url,
+ :color_scheme_id, :theme_id, :avatar, :hide_no_ssh_key,
+ :hide_no_password, :location, :public_email
)
end
end
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index 679a5d76ec0..6fba3ce299b 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -8,7 +8,14 @@ class Projects::DeployKeysController < Projects::ApplicationController
def index
@enabled_keys = @project.deploy_keys
- @available_keys = available_keys - @enabled_keys
+
+ @available_keys = accessible_keys - @enabled_keys
+ @available_project_keys = current_user.project_deploy_keys - @enabled_keys
+ @available_public_keys = DeployKey.are_public - @enabled_keys
+
+ # Public keys that are already used by another accessible project are already
+ # in @available_project_keys.
+ @available_public_keys -= @available_project_keys
end
def show
@@ -32,18 +39,9 @@ class Projects::DeployKeysController < Projects::ApplicationController
end
end
- def destroy
- @key = @project.deploy_keys.find(params[:id])
- @key.destroy
-
- respond_to do |format|
- format.html { redirect_to namespace_project_deploy_keys_path(@project.namespace, @project) }
- format.js { render nothing: true }
- end
- end
-
def enable
- @project.deploy_keys << available_keys.find(params[:id])
+ @key = accessible_keys.find(params[:id])
+ @project.deploy_keys << @key
redirect_to namespace_project_deploy_keys_path(@project.namespace,
@project)
@@ -52,14 +50,13 @@ class Projects::DeployKeysController < Projects::ApplicationController
def disable
@project.deploy_keys_projects.find_by(deploy_key_id: params[:id]).destroy
- redirect_to namespace_project_deploy_keys_path(@project.namespace,
- @project)
+ redirect_to :back
end
protected
- def available_keys
- @available_keys ||= current_user.accessible_deploy_keys
+ def accessible_keys
+ @accessible_keys ||= current_user.accessible_deploy_keys
end
def deploy_key_params
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 4ab15db01f7..72967a26ff1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -6,6 +6,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def index
@project_members = @project.project_members
+ @project_members = @project_members.non_invite unless can?(current_user, :admin_project, @project)
if params[:search].present?
users = @project.users.search(params[:search]).to_a
@@ -17,6 +18,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group = @project.group
if @group
@group_members = @group.group_members
+ @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
if params[:search].present?
users = @group.users.search(params[:search]).to_a
@@ -34,30 +36,42 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- users = User.where(id: params[:user_ids].split(','))
- @project.team << [users, params[:access_level]]
+ @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
def update
- @project_member = @project.project_members.find_by(user_id: member)
+ @project_member = @project.project_members.find(params[:id])
@project_member.update_attributes(member_params)
end
def destroy
- @project_member = @project.project_members.find_by(user_id: member)
+ @project_member = @project.project_members.find(params[:id])
@project_member.destroy
respond_to do |format|
format.html do
- redirect_to namespace_project_project_members_path(@project.namespace,
- @project)
+ redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
format.js { render nothing: true }
end
end
+ def resend_invite
+ redirect_path = namespace_project_project_members_path(@project.namespace, @project)
+
+ @project_member = @project.project_members.find(params[:id])
+
+ if @project_member.invite?
+ @project_member.resend_invite
+
+ redirect_to redirect_path, notice: 'The invitation was successfully resent.'
+ else
+ redirect_to redirect_path, alert: 'The invitation has already been accepted.'
+ end
+ end
+
def leave
@project.project_members.find_by(user_id: current_user).destroy
@@ -69,7 +83,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def apply_import
giver = Project.find(params[:source_project_id])
- status = @project.team.import(giver)
+ status = @project.team.import(giver, current_user)
notice = status ? "Successfully imported" : "Import failed"
redirect_to(namespace_project_project_members_path(project.namespace, project),
@@ -78,10 +92,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
- def member
- @member ||= User.find_by(username: params[:id])
- end
-
def member_params
params.require(:project_member).permit(:user_id, :access_level)
end
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 67acf45ab7f..ec3b2b8d75a 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -55,5 +55,10 @@ class Projects::RefsController < Projects::ApplicationController
commit: last_commit
}
end
+
+ respond_to do |format|
+ format.html { render_404 }
+ format.js
+ end
end
end
diff --git a/app/controllers/projects/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb
index cbb888b25e8..96defb0c721 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -11,18 +11,18 @@ class Projects::RepositoriesController < Projects::ApplicationController
end
def archive
- unless can?(current_user, :download_code, @project)
- render_404 and return
+ begin
+ file_path = ArchiveRepositoryService.new(@project, params[:ref], params[:format]).execute
+ rescue
+ return head :not_found
end
- file_path = ArchiveRepositoryService.new.execute(@project, params[:ref], params[:format])
-
if file_path
# Send file to user
response.headers["Content-Length"] = File.open(file_path).size.to_s
send_file file_path
else
- render_404
+ redirect_to request.fullpath
end
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 643167947b9..aeb7f0699f5 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -5,6 +5,7 @@ class Projects::WikisController < Projects::ApplicationController
before_filter :authorize_write_wiki!, only: [:edit, :create, :history]
before_filter :authorize_admin_wiki!, only: :destroy
before_filter :load_project_wiki
+ include WikiHelper
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
@@ -45,7 +46,10 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :write_wiki, @project)
if @page.update(content, format, message)
- redirect_to [@project.namespace.becomes(Namespace), @project, @page], notice: 'Wiki was successfully updated.'
+ redirect_to(
+ namespace_project_wiki_path(@project.namespace, @project, @page),
+ notice: 'Wiki was successfully updated.'
+ )
else
render 'edit'
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index a3284c82d3f..c5828d0b2df 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -3,15 +3,22 @@ class SearchController < ApplicationController
def show
return if params[:search].nil? || params[:search].blank?
- @project = Project.find_by(id: params[:project_id]) if params[:project_id].present?
- @group = Group.find_by(id: params[:group_id]) if params[:group_id].present?
+
+ if params[:project_id].present?
+ @project = Project.find_by(id: params[:project_id])
+ @project = nil unless can?(current_user, :download_code, @project)
+ end
+
+ if params[:group_id].present?
+ @group = Group.find_by(id: params[:group_id])
+ @group = nil unless can?(current_user, :read_group, @group)
+ end
+
@scope = params[:scope]
@show_snippets = params[:snippets].eql? 'true'
@search_results =
if @project
- return access_denied! unless can?(current_user, :download_code, @project)
-
unless %w(blobs notes issues merge_requests wiki_blobs).
include?(@scope)
@scope = 'blobs'
@@ -35,7 +42,12 @@ class SearchController < ApplicationController
def autocomplete
term = params[:term]
- @project = Project.find(params[:project_id]) if params[:project_id].present?
+
+ if params[:project_id].present?
+ @project = Project.find_by(id: params[:project_id])
+ @project = nil unless can?(current_user, :read_project, @project)
+ end
+
@ref = params[:project_ref] if params[:project_ref].present?
render json: search_autocomplete_opts(term).to_json
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 7b6982c5074..3f11d7afe6f 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -26,6 +26,12 @@ class SessionsController < Devise::SessionsController
end
def create
- super
+ super do |resource|
+ # User has successfully signed in, so clear any unused reset tokens
+ if resource.reset_password_token.present?
+ resource.update_attributes(reset_password_token: nil,
+ reset_password_sent_at: nil)
+ end
+ end
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 088a766ed3a..2c0702073d4 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -19,6 +19,8 @@
require_relative 'projects_finder'
class IssuableFinder
+ NONE = '0'
+
attr_accessor :current_user, :params
def execute(current_user, params)
@@ -112,7 +114,7 @@ class IssuableFinder
def by_milestone(items)
if params[:milestone_id].present?
- items = items.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id]))
+ items = items.where(milestone_id: (params[:milestone_id] == NONE ? nil : params[:milestone_id]))
end
items
@@ -120,7 +122,7 @@ class IssuableFinder
def by_assignee(items)
if params[:assignee_id].present?
- items = items.where(assignee_id: (params[:assignee_id] == '0' ? nil : params[:assignee_id]))
+ items = items.where(assignee_id: (params[:assignee_id] == NONE ? nil : params[:assignee_id]))
end
items
@@ -128,7 +130,7 @@ class IssuableFinder
def by_author(items)
if params[:author_id].present?
- items = items.where(author_id: (params[:author_id] == '0' ? nil : params[:author_id]))
+ items = items.where(author_id: (params[:author_id] == NONE ? nil : params[:author_id]))
end
items
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 38b5fc4a011..20457572a08 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -125,7 +125,7 @@ module ApplicationHelper
# If reference is commit id - we should add it to branch/tag selectbox
if(@ref && !options.flatten.include?(@ref) &&
- @ref =~ /^[0-9a-zA-Z]{6,52}$/)
+ @ref =~ /\A[0-9a-zA-Z]{6,52}\z/)
options << ['Commit', [@ref]]
end
@@ -236,33 +236,35 @@ module ApplicationHelper
Gitlab::MarkdownHelper.gitlab_markdown?(filename)
end
- def link_to(name = nil, options = nil, html_options = nil, &block)
- begin
- uri = URI(options)
- host = uri.host
- absolute_uri = uri.absolute?
- rescue URI::InvalidURIError, ArgumentError
- host = nil
- absolute_uri = nil
- end
-
- # Add 'nofollow' only to external links
- if host && host != Gitlab.config.gitlab.host && absolute_uri
- if html_options
- if html_options[:rel]
- html_options[:rel] << ' nofollow'
- else
- html_options.merge!(rel: 'nofollow')
- end
- else
- html_options = Hash.new
- html_options[:rel] = 'nofollow'
+ # Overrides ActionView::Helpers::UrlHelper#link_to to add `rel="nofollow"` to
+ # external links
+ def link_to(name = nil, options = nil, html_options = {})
+ if options.kind_of?(String)
+ if !options.start_with?('#', '/')
+ html_options = add_nofollow(options, html_options)
end
end
super
end
+ # Add `"rel=nofollow"` to external links
+ #
+ # link - String link to check
+ # html_options - Hash of `html_options` passed to `link_to`
+ #
+ # Returns `html_options`, adding `rel: nofollow` for external links
+ def add_nofollow(link, html_options = {})
+ uri = URI(link)
+
+ if uri && uri.absolute? && uri.host != Gitlab.config.gitlab.host
+ rel = html_options.fetch(:rel, '')
+ html_options[:rel] = (rel + ' nofollow').strip
+ end
+
+ html_options
+ end
+
def escaped_autolink(text)
auto_link ERB::Util.html_escape(text), link: :urls
end
@@ -275,7 +277,9 @@ module ApplicationHelper
'https://' + promo_host
end
- def page_filter_path(options={})
+ def page_filter_path(options = {})
+ without = options.delete(:without)
+
exist_opts = {
state: params[:state],
scope: params[:scope],
@@ -288,6 +292,12 @@ module ApplicationHelper
options = exist_opts.merge(options)
+ if without.present?
+ without.each do |key|
+ options.delete(key)
+ end
+ end
+
path = request.path
path << "?#{options.to_param}"
path
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 798d62b3a09..4ea838ca447 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -61,4 +61,12 @@ module BlobHelper
'Preview changes'
end
end
+
+ # Return an image icon depending on the file mode and extension
+ #
+ # mode - File unix mode
+ # mode - File name
+ def blob_icon(mode, name)
+ icon("#{file_type_icon_class('file', mode, name)} fw")
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 5aae697e2f0..d13d80be293 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -134,12 +134,13 @@ module CommitsHelper
# avatar: true will prepend the avatar image
# size: size of the avatar image in px
def commit_person_link(commit, options = {})
+ user = commit.send(options[:source])
+
source_name = clean(commit.send "#{options[:source]}_name".to_sym)
source_email = clean(commit.send "#{options[:source]}_email".to_sym)
- user = User.find_for_commit(source_email, source_name)
- person_name = user.nil? ? source_name : user.name
- person_email = user.nil? ? source_email : user.email
+ person_name = user.try(:name) || source_name
+ person_email = user.try(:email) || source_email
text =
if options[:avatar]
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index b56f21c7a18..4f42972a4dd 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -101,7 +101,7 @@ module DiffHelper
end
def line_comments
- @line_comments ||= @line_notes.group_by(&:line_code)
+ @line_comments ||= @line_notes.select(&:active?).group_by(&:line_code)
end
def organize_comments(type_left, type_right, line_code_left, line_code_right)
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 08476f8516e..0df3ecc90b7 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -1,6 +1,3 @@
-require 'html/pipeline'
-require 'html/pipeline/gitlab'
-
module EmailsHelper
# Google Actions
@@ -33,35 +30,9 @@ module EmailsHelper
end
end
- def add_email_highlight_css
- Rugments::Themes::Github.render(scope: '.highlight')
- end
-
def color_email_diff(diffcontent)
- formatter = Rugments::Formatters::HTML.new(cssclass: 'highlight')
+ formatter = Rugments::Formatters::HTML.new(cssclass: "highlight", inline_theme: :github)
lexer = Rugments::Lexers::Diff.new
raw formatter.format(lexer.lex(diffcontent))
end
-
- def replace_image_links_with_base64(text, project)
- # Used pipelines in GitLab:
- # GitlabEmailImageFilter - replaces images that have been uploaded as attachments with inline images in emails.
- #
- # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
- filters = [
- HTML::Pipeline::Gitlab::GitlabEmailImageFilter
- ]
-
- context = {
- base_url: File.join(Gitlab.config.gitlab.url, project.path_with_namespace, 'uploads'),
- upload_path: File.join(Rails.root, 'public', 'uploads', project.path_with_namespace),
- }
-
- pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
-
- result = pipeline.call(text, context)
- text = result[:output].to_html(save_with: 0)
-
- text.html_safe
- end
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 7ca3f058636..aa1de2f50ef 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -13,7 +13,7 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
- escaped_body = if body =~ /^\<img/
+ escaped_body = if body =~ /\A\<img/
body
else
escape_once(body)
@@ -31,24 +31,28 @@ module GitlabMarkdownHelper
def markdown(text, options={})
unless @markdown && options == @options
@options = options
- gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self,
- user_color_scheme_class,
- {
- # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch-
- with_toc_data: true,
- safe_links_only: true
- }.merge(options))
- @markdown = Redcarpet::Markdown.new(gitlab_renderer,
- # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
- no_intra_emphasis: true,
- tables: true,
- fenced_code_blocks: true,
- autolink: true,
- strikethrough: true,
- lax_spacing: true,
- space_after_headers: true,
- superscript: true)
+
+ # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch
+ rend = Redcarpet::Render::GitlabHTML.new(self, user_color_scheme_class, {
+ with_toc_data: true,
+ safe_links_only: true,
+ # Handled further down the line by HTML::Pipeline::SanitizationFilter
+ escape_html: false
+ }.merge(options))
+
+ # see https://github.com/vmg/redcarpet#and-its-like-really-simple-to-use
+ @markdown = Redcarpet::Markdown.new(rend,
+ no_intra_emphasis: true,
+ tables: true,
+ fenced_code_blocks: true,
+ autolink: true,
+ strikethrough: true,
+ lax_spacing: true,
+ space_after_headers: true,
+ superscript: true
+ )
end
+
@markdown.render(text).html_safe
end
@@ -135,7 +139,7 @@ module GitlabMarkdownHelper
@project.path_with_namespace,
path_with_ref(file_path),
file_path
- ].compact.join("/").gsub(/^\/*|\/*$/, '') + id
+ ].compact.join("/").gsub(/\A\/*|\/*\z/, '') + id
end
def sanitize_slashes(path)
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 3386fac8657..9703c8d9e9c 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -29,6 +29,10 @@ module GitlabRoutingHelper
namespace_project_merge_request_path(entity.project.namespace, entity.project, entity, *args)
end
+ def milestone_path(entity, *args)
+ namespace_project_milestone_path(entity.project.namespace, entity.project, entity, *args)
+ end
+
def project_url(project, *args)
namespace_project_url(project.namespace, project, *args)
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 2d0d0b494f6..add0a776a63 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -1,6 +1,10 @@
module GroupsHelper
- def remove_user_from_group_message(group, user)
- "Are you sure you want to remove \"#{user.name}\" from \"#{group.name}\"?"
+ def remove_user_from_group_message(group, member)
+ if member.user
+ "Are you sure you want to remove \"#{member.user.name}\" from \"#{group.name}\"?"
+ else
+ "Are you sure you want to revoke the invitation for \"#{member.invite_email}\" to join \"#{group.name}\"?"
+ end
end
def leave_group_message(group)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 18260f0ed4d..a9030729b48 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -36,4 +36,48 @@ module IconsHelper
def private_icon
icon('lock')
end
+
+ def file_type_icon_class(type, mode, name)
+ if type == 'folder'
+ icon_class = 'folder'
+ elsif mode == '120000'
+ icon_class = 'share'
+ else
+ # Guess which icon to choose based on file extension.
+ # If you think a file extension is missing, feel free to add it on PR
+
+ case File.extname(name).downcase
+ when '.pdf'
+ icon_class = 'file-pdf-o'
+ when '.jpg', '.jpeg', '.jif', '.jfif',
+ '.jp2', '.jpx', '.j2k', '.j2c',
+ '.png', '.gif', '.tif', '.tiff',
+ '.svg', '.ico', '.bmp'
+ icon_class = 'file-image-o'
+ when '.zip', '.zipx', '.tar', '.gz', '.bz', '.bzip',
+ '.xz', '.rar', '.7z'
+ icon_class = 'file-archive-o'
+ when '.mp3', '.wma', '.ogg', '.oga', '.wav', '.flac', '.aac'
+ icon_class = 'file-audio-o'
+ when '.mp4', '.m4p', '.m4v',
+ '.mpg', '.mp2', '.mpeg', '.mpe', '.mpv',
+ '.mpg', '.mpeg', '.m2v',
+ '.avi', '.mkv', '.flv', '.ogv', '.mov',
+ '.3gp', '.3g2'
+ icon_class = 'file-video-o'
+ when '.doc', '.dot', '.docx', '.docm', '.dotx', '.dotm', '.docb'
+ icon_class = 'file-word-o'
+ when '.xls', '.xlt', '.xlm', '.xlsx', '.xlsm', '.xltx', '.xltm',
+ '.xlsb', '.xla', '.xlam', '.xll', '.xlw'
+ icon_class = 'file-excel-o'
+ when '.ppt', '.pot', '.pps', '.pptx', '.pptm', '.potx', '.potm',
+ '.ppam', '.ppsx', '.ppsm', '.sldx', '.sldm'
+ icon_class = 'file-powerpoint-o'
+ else
+ icon_class = 'file-text-o'
+ end
+ end
+
+ icon_class
+ end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index a4bd4d30215..ad4a7612724 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -13,22 +13,34 @@ module IssuesHelper
OpenStruct.new(id: 0, title: 'None (backlog)', name: 'Unassigned')
end
- def url_for_project_issues(project = @project)
+ def url_for_project_issues(project = @project, options = {})
return '' if project.nil?
- project.issues_tracker.project_url
+ if options[:only_path]
+ project.issues_tracker.project_path
+ else
+ project.issues_tracker.project_url
+ end
end
- def url_for_new_issue(project = @project)
+ def url_for_new_issue(project = @project, options = {})
return '' if project.nil?
- project.issues_tracker.new_issue_url
+ if options[:only_path]
+ project.issues_tracker.new_issue_path
+ else
+ project.issues_tracker.new_issue_url
+ end
end
- def url_for_issue(issue_iid, project = @project)
+ def url_for_issue(issue_iid, project = @project, options = {})
return '' if project.nil?
- project.issues_tracker.issue_url(issue_iid)
+ if options[:only_path]
+ project.issues_tracker.issue_path(issue_iid)
+ else
+ project.issues_tracker.issue_url(issue_iid)
+ end
end
def title_for_issue(issue_iid, project = @project)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 49063491abf..32ef2e7ca84 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -47,4 +47,8 @@ module LabelsHelper
"#FFF"
end
end
+
+ def project_labels_options(project)
+ options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
+ end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 59fdc0d49cc..282bdf744d2 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -19,4 +19,15 @@ module MilestonesHelper
content_tag :div, nil, options
end
end
+
+ def projects_milestones_options
+ milestones =
+ if @project
+ @project.milestones
+ else
+ Milestone.where(project_id: @projects)
+ end.active
+
+ options_from_collection_for_select(milestones, 'id', 'title', params[:milestone_id])
+ end
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7bf51b5b8e8..c2a7732e6f0 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -1,6 +1,10 @@
module ProjectsHelper
- def remove_from_project_team_message(project, user)
- "You are going to remove #{user.name} from #{project.name} project team. Are you sure?"
+ def remove_from_project_team_message(project, member)
+ if member.user
+ "You are going to remove #{member.user.name} from #{project.name} project team. Are you sure?"
+ else
+ "You are going to revoke the invitation for #{member.invite_email} to join #{project.name} project team. Are you sure?"
+ end
end
def link_to_project(project)
@@ -80,17 +84,17 @@ module ProjectsHelper
@project.milestones.active.order("due_date, title ASC")
end
- def link_to_toggle_star(title, starred, signed_in)
- cls = 'star-btn'
- cls << ' disabled' unless signed_in
+ def link_to_toggle_star(title, starred)
+ cls = 'star-btn btn btn-sm btn-default'
- toggle_html = content_tag('span', class: 'toggle') do
- toggle_text = if starred
- ' Unstar'
- else
- ' Star'
- end
+ toggle_text =
+ if starred
+ ' Unstar'
+ else
+ ' Star'
+ end
+ toggle_html = content_tag('span', class: 'toggle') do
icon('star') + toggle_text
end
@@ -106,23 +110,33 @@ module ProjectsHelper
data: { type: 'json' }
}
+ path = toggle_star_namespace_project_path(@project.namespace, @project)
content_tag 'span', class: starred ? 'turn-on' : 'turn-off' do
- link_to(
- toggle_star_namespace_project_path(@project.namespace, @project),
- link_opts
- ) do
+ link_to(path, link_opts) do
toggle_html + ' ' + count_html
end
end
end
def link_to_toggle_fork
- out = icon('code-fork')
- out << ' Fork'
- out << content_tag(:span, class: 'count') do
+ html = content_tag('span') do
+ icon('code-fork') + ' Fork'
+ end
+
+ count_html = content_tag(:span, class: 'count') do
@project.forks_count.to_s
end
+
+ html + count_html
+ end
+
+ def project_for_deploy_key(deploy_key)
+ if deploy_key.projects.include?(@project)
+ @project
+ else
+ deploy_key.projects.find { |project| can?(current_user, :read_project, project) }
+ end
end
private
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 796d805f219..bec8f2f1aa7 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -4,18 +4,31 @@ module SelectsHelper
css_class << "multiselect " if opts[:multiple]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
+ placeholder = opts[:placeholder] || 'Search for a user'
- hidden_field_tag(id, value, class: css_class)
- end
+ null_user = opts[:null_user] || false
+ any_user = opts[:any_user] || false
+ email_user = opts[:email_user] || false
+ first_user = opts[:first_user] && current_user ? current_user.username : false
- def project_users_select_tag(id, opts = {})
- css_class = "ajax-project-users-select "
- css_class << "multiselect " if opts[:multiple]
- css_class << (opts[:class] || '')
- value = opts[:selected] || ''
- placeholder = opts[:placeholder] || 'Select user'
- project_id = opts[:project_id] || @project.id
- hidden_field_tag(id, value, class: css_class, 'data-placeholder' => placeholder, 'data-project-id' => project_id)
+ html = {
+ class: css_class,
+ 'data-placeholder' => placeholder,
+ 'data-null-user' => null_user,
+ 'data-any-user' => any_user,
+ 'data-email-user' => email_user,
+ 'data-first-user' => first_user
+ }
+
+ unless opts[:scope] == :all
+ if @project
+ html['data-project-id'] = @project.id
+ elsif @group
+ html['data-group-id'] = @group.id
+ end
+ end
+
+ hidden_field_tag(id, value, html)
end
def groups_select_tag(id, opts = {})
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 241462e5e4c..9954617c762 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -44,7 +44,7 @@ module SubmoduleHelper
def relative_self_url?(url)
# (./)?(../repo.git) || (./)?(../../project/repo.git) )
- url =~ /^((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\Z/ || url =~ /^((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\Z/
+ url =~ /\A((\.\/)?(\.\.\/))(?!(\.\.)|(.*\/)).*\.git\z/ || url =~ /\A((\.\/)?(\.\.\/){2})(?!(\.\.))([^\/]*)\/(?!(\.\.)|(.*\/)).*\.git\z/
end
def standard_links(host, namespace, project, commit)
@@ -53,15 +53,22 @@ module SubmoduleHelper
end
def relative_self_links(url, commit)
- if url.scan(/(\.\.\/)/).size == 2
- base = url[/([^\/]*\/[^\/]*)\.git/, 1]
- else
- base = [ @project.group.path, '/', url[/([^\/]*)\.git/, 1] ].join('')
+ # Map relative links to a namespace and project
+ # For example:
+ # ../bar.git -> same namespace, repo bar
+ # ../foo/bar.git -> namespace foo, repo bar
+ # ../../foo/bar/baz.git -> namespace bar, repo baz
+ components = url.split('/')
+ base = components.pop.gsub(/.git$/, '')
+ namespace = components.pop.gsub(/^\.\.$/, '')
+
+ if namespace.empty?
+ namespace = @project.group.path
end
[
- namespace_project_path(base.namespace, base),
- namespace_project_tree_path(base.namespace, base, commit)
+ namespace_project_path(namespace, base),
+ namespace_project_tree_path(namespace, base, commit)
]
end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index bf6726574ec..6dd9b6f017c 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -34,12 +34,13 @@ module TreeHelper
end
end
- # Return an image icon depending on the file type
+ # Return an image icon depending on the file type and mode
#
# type - String type of the tree item; either 'folder' or 'file'
- def tree_icon(type)
- icon_class = type == 'folder' ? 'folder' : 'file-o'
- icon(icon_class)
+ # mode - File unix mode
+ # name - File name
+ def tree_icon(type, mode, name)
+ icon("#{file_type_icon_class(type, mode, name)} fw")
end
def tree_hex_class(content)
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
new file mode 100644
index 00000000000..a3bc64c010e
--- /dev/null
+++ b/app/helpers/wiki_helper.rb
@@ -0,0 +1,22 @@
+module WikiHelper
+ # Rails v4.1.9+ escapes all model IDs, converting slashes into %2F. The
+ # only way around this is to implement our own path generators.
+ def namespace_project_wiki_path(namespace, project, wiki_page, *args)
+ slug =
+ case wiki_page
+ when Symbol
+ wiki_page
+ else
+ wiki_page.slug
+ end
+ namespace_project_path(namespace, project) + "/wikis/#{slug}"
+ end
+
+ def edit_namespace_project_wiki_path(namespace, project, wiki_page, *args)
+ namespace_project_wiki_path(namespace, project, wiki_page) + '/edit'
+ end
+
+ def history_namespace_project_wiki_path(namespace, project, wiki_page, *args)
+ namespace_project_wiki_path(namespace, project, wiki_page) + '/history'
+ end
+end
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
index 26f43bf955e..1c43f95dc8c 100644
--- a/app/mailers/emails/groups.rb
+++ b/app/mailers/emails/groups.rb
@@ -3,9 +3,50 @@ module Emails
def group_access_granted_email(group_member_id)
@group_member = GroupMember.find(group_member_id)
@group = @group_member.group
+
@target_url = group_url(@group)
- mail(to: @group_member.user.email,
+ @current_user = @group_member.user
+
+ mail(to: @group_member.user.notification_email,
subject: subject("Access to group was granted"))
end
+
+ def group_member_invited_email(group_member_id, token)
+ @group_member = GroupMember.find group_member_id
+ @group = @group_member.group
+ @token = token
+
+ @target_url = group_url(@group)
+ @current_user = @group_member.user
+
+ mail(to: @group_member.invite_email,
+ subject: "Invitation to join group #{@group.name}")
+ end
+
+ def group_invite_accepted_email(group_member_id)
+ @group_member = GroupMember.find group_member_id
+ return if @group_member.created_by.nil?
+
+ @group = @group_member.group
+
+ @target_url = group_url(@group)
+ @current_user = @group_member.created_by
+
+ mail(to: @group_member.created_by.notification_email,
+ subject: subject("Invitation accepted"))
+ end
+
+ def group_invite_declined_email(group_id, invite_email, access_level, created_by_id)
+ return if created_by_id.nil?
+
+ @group = Group.find(group_id)
+ @current_user = @created_by = User.find(created_by_id)
+ @access_level = access_level
+ @invite_email = invite_email
+
+ @target_url = group_url(@group)
+ mail(to: @created_by.notification_email,
+ subject: subject("Invitation declined"))
+ end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index ab5b0765352..3a83b083109 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -1,7 +1,7 @@
module Emails
module Profile
def new_user_email(user_id, token = nil)
- @user = User.find(user_id)
+ @current_user = @user = User.find(user_id)
@target_url = user_url(@user)
@token = token
mail(to: @user.notification_email, subject: subject("Account was created for you"))
@@ -9,13 +9,13 @@ module Emails
def new_email_email(email_id)
@email = Email.find(email_id)
- @user = @email.user
+ @current_user = @user = @email.user
mail(to: @user.notification_email, subject: subject("Email was added to your account"))
end
def new_ssh_key_email(key_id)
@key = Key.find(key_id)
- @user = @key.user
+ @current_user = @user = @key.user
@target_url = user_url(@user)
mail(to: @user.notification_email, subject: subject("SSH key was added to your account"))
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 3cd812825e2..0dbb2939bb3 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,15 +1,57 @@
module Emails
module Projects
- def project_access_granted_email(user_project_id)
- @project_member = ProjectMember.find user_project_id
+ def project_access_granted_email(project_member_id)
+ @project_member = ProjectMember.find project_member_id
@project = @project_member.project
+
@target_url = namespace_project_url(@project.namespace, @project)
- mail(to: @project_member.user.email,
+ @current_user = @project_member.user
+
+ mail(to: @project_member.user.notification_email,
subject: subject("Access to project was granted"))
end
+ def project_member_invited_email(project_member_id, token)
+ @project_member = ProjectMember.find project_member_id
+ @project = @project_member.project
+ @token = token
+
+ @target_url = namespace_project_url(@project.namespace, @project)
+ @current_user = @project_member.user
+
+ mail(to: @project_member.invite_email,
+ subject: "Invitation to join project #{@project.name_with_namespace}")
+ end
+
+ def project_invite_accepted_email(project_member_id)
+ @project_member = ProjectMember.find project_member_id
+ return if @project_member.created_by.nil?
+
+ @project = @project_member.project
+
+ @target_url = namespace_project_url(@project.namespace, @project)
+ @current_user = @project_member.created_by
+
+ mail(to: @project_member.created_by.notification_email,
+ subject: subject("Invitation accepted"))
+ end
+
+ def project_invite_declined_email(project_id, invite_email, access_level, created_by_id)
+ return if created_by_id.nil?
+
+ @project = Project.find(project_id)
+ @current_user = @created_by = User.find(created_by_id)
+ @access_level = access_level
+ @invite_email = invite_email
+
+ @target_url = namespace_project_url(@project.namespace, @project)
+
+ mail(to: @created_by.notification_email,
+ subject: subject("Invitation declined"))
+ end
+
def project_was_moved_email(project_id, user_id)
- @user = User.find user_id
+ @current_user = @user = User.find user_id
@project = Project.find project_id
@target_url = namespace_project_url(@project.namespace, @project)
mail(to: @user.notification_email,
@@ -28,7 +70,7 @@ module Emails
end
@project = Project.find(project_id)
- @author = User.find(author_id)
+ @current_user = @author = User.find(author_id)
@reverse_compare = reverse_compare
@compare = compare
@ref_name = Gitlab::Git.ref_name(ref)
@@ -83,9 +125,17 @@ module Emails
@disable_footer = true
- mail(from: sender(author_id, send_from_committer_email),
- to: recipient,
- subject: @subject)
+ reply_to =
+ if send_from_committer_email && can_send_from_user_email?(@author)
+ @author.email
+ else
+ Gitlab.config.gitlab.email_reply_to
+ end
+
+ mail(from: sender(author_id, send_from_committer_email),
+ reply_to: reply_to,
+ to: recipient,
+ subject: @subject)
end
end
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 8fcdd3bc853..2c0d451511f 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -13,13 +13,16 @@ class Notify < ActionMailer::Base
add_template_helper MergeRequestsHelper
add_template_helper EmailsHelper
+ attr_accessor :current_user
+ helper_method :current_user, :can?
+
default_url_options[:host] = Gitlab.config.gitlab.host
default_url_options[:protocol] = Gitlab.config.gitlab.protocol
default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
default from: Proc.new { default_sender_address.format }
- default reply_to: "noreply@#{Gitlab.config.gitlab.host}"
+ default reply_to: Gitlab.config.gitlab.email_reply_to
# Just send email with 2 seconds delay
def self.delay
@@ -57,20 +60,24 @@ class Notify < ActionMailer::Base
address
end
+ def can_send_from_user_email?(sender)
+ sender_domain = sender.email.split("@").last
+ self.class.allowed_email_domains.include?(sender_domain)
+ end
+
# Return an email address that displays the name of the sender.
# Only the displayed name changes; the actual email address is always the same.
def sender(sender_id, send_from_user_email = false)
- if sender = User.find(sender_id)
- address = default_sender_address
- address.display_name = sender.name
-
- sender_domain = sender.email.split("@").last
- if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain)
- address.address = sender.email
- end
+ return unless sender = User.find(sender_id)
+
+ address = default_sender_address
+ address.display_name = sender.name
- address.format
+ if send_from_user_email && can_send_from_user_email?(sender)
+ address.address = sender.email
end
+
+ address.format
end
# Look up a User by their ID and return their email address
@@ -79,9 +86,8 @@ class Notify < ActionMailer::Base
#
# Returns a String containing the User's email address.
def recipient(recipient_id)
- if recipient = User.find(recipient_id)
- recipient.notification_email
- end
+ @current_user = User.find(recipient_id)
+ @current_user.notification_email
end
# Set the References header field
@@ -154,4 +160,8 @@ class Notify < ActionMailer::Base
mail(headers, &block)
end
+
+ def can?
+ Ability.abilities.allowed?(user, action, subject)
+ end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index d2b39f667f2..85a15596f8d 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -198,11 +198,11 @@ class Ability
])
end
- # Only group owner and administrators can manage group
+ # Only group owner and administrators can admin group
if group.has_owner?(user) || user.admin?
rules.push(*[
- :manage_group,
- :manage_namespace
+ :admin_group,
+ :admin_namespace
])
end
@@ -212,11 +212,11 @@ class Ability
def namespace_abilities(user, namespace)
rules = []
- # Only namespace owner and administrators can manage it
+ # Only namespace owner and administrators can admin it
if namespace.owner == user || user.admin?
rules.push(*[
:create_projects,
- :manage_namespace
+ :admin_namespace
])
end
@@ -254,7 +254,7 @@ class Ability
rules = []
target_user = subject.user
group = subject.group
- can_manage = group_abilities(user, group).include?(:manage_group)
+ can_manage = group_abilities(user, group).include?(:admin_group)
if can_manage && (user != target_user)
rules << :modify_group_member
rules << :destroy_group_member
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 1c87db613ae..0d8365c4ff2 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -16,6 +16,7 @@
# default_branch_protection :integer default(2)
# twitter_sharing_enabled :boolean default(TRUE)
# restricted_visibility_levels :text
+# max_attachment_size :integer default(10)
#
class ApplicationSetting < ActiveRecord::Base
@@ -23,7 +24,7 @@ class ApplicationSetting < ActiveRecord::Base
validates :home_page_url,
allow_blank: true,
- format: { with: URI::regexp(%w(http https)), message: "should be a valid url" },
+ format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" },
if: :home_page_url_column_exist
validates_each :restricted_visibility_levels do |record, attr, value|
@@ -49,7 +50,8 @@ class ApplicationSetting < ActiveRecord::Base
twitter_sharing_enabled: Settings.gitlab['twitter_sharing_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels']
+ restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
+ max_attachment_size: Settings.gitlab['max_attachment_size']
)
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index e0461809e10..006fa62c8f9 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -77,7 +77,7 @@ class Commit
title_end = title.index("\n")
if (!title_end && title.length > 100) || (title_end && title_end > 100)
- title[0..79] << "&hellip;".html_safe
+ title[0..79] << "…"
else
title.split("\n", 2).first
end
@@ -90,7 +90,7 @@ class Commit
title_end = safe_message.index("\n")
@description ||=
if (!title_end && safe_message.length > 100) || (title_end && title_end > 100)
- "&hellip;".html_safe << safe_message[80..-1]
+ "…" << safe_message[80..-1]
else
safe_message.split("\n", 2)[1].try(:chomp)
end
@@ -117,8 +117,8 @@ class Commit
# Discover issues should be closed when this commit is pushed to a project's
# default branch.
- def closes_issues(project)
- Gitlab::ClosingIssueExtractor.closed_by_message_in_project(safe_message, project)
+ def closes_issues(project, current_user = self.committer)
+ Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
# Mentionable override.
@@ -126,6 +126,14 @@ class Commit
"commit #{id}"
end
+ def author
+ User.find_for_commit(author_email, author_name)
+ end
+
+ def committer
+ User.find_for_commit(committer_email, committer_name)
+ end
+
def method_missing(m, *args, &block)
@raw.send(m, *args, &block)
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 88ac83744df..478134dff68 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -118,16 +118,16 @@ module Issuable
end
# Return all users participating on the discussion
- def participants
+ def participants(current_user = self.author)
users = []
users << author
users << assignee if is_assigned?
mentions = []
- mentions << self.mentioned_users
+ mentions << self.mentioned_users(current_user)
notes.each do |note|
users << note.author
- mentions << note.mentioned_users
+ mentions << note.mentioned_users(current_user)
end
users.concat(mentions.reduce([], :|)).uniq
@@ -140,7 +140,7 @@ module Issuable
return subscription.subscribed
end
- participants.include?(user)
+ participants(user).include?(user)
end
def toggle_subscription(user)
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 74900d4675d..b7882a2bb16 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -42,35 +42,22 @@ module Mentionable
Note.cross_reference_exists?(target, local_reference)
end
- def mentioned_users
- users = []
- return users if mentionable_text.blank?
- has_project = self.respond_to? :project
- matches = mentionable_text.scan(/@[a-zA-Z][a-zA-Z0-9_\-\.]*/)
- matches.each do |match|
- identifier = match.delete "@"
- if identifier == "all"
- users.push(*project.team.members.flatten)
- elsif namespace = Namespace.find_by(path: identifier)
- if namespace.type == "Group"
- users.push(*namespace.users)
- else
- users << namespace.owner
- end
- end
- end
- users.uniq
+ def mentioned_users(current_user = nil)
+ return [] if mentionable_text.blank?
+
+ ext = Gitlab::ReferenceExtractor.new(self.project, current_user)
+ ext.analyze(mentionable_text)
+ ext.users.uniq
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
- def references(p = project, text = mentionable_text)
+ def references(p = project, current_user = self.author, text = mentionable_text)
return [] if text.blank?
- ext = Gitlab::ReferenceExtractor.new
- ext.analyze(text, p)
- (ext.issues_for(p) +
- ext.merge_requests_for(p) +
- ext.commits_for(p)).uniq - [local_reference]
+ ext = Gitlab::ReferenceExtractor.new(p, current_user)
+ ext.analyze(text)
+
+ (ext.issues + ext.merge_requests + ext.commits).uniq - [local_reference]
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in +mentionable_text+.
@@ -96,7 +83,7 @@ module Mentionable
# Only proceed if the saved changes actually include a chance to an attr_mentionable field.
return unless mentionable_changed
- preexisting = references(p, original)
+ preexisting = references(p, self.author, original)
create_cross_references!(p, a, preexisting)
end
end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 570f5e91c13..85d52d558cd 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -7,6 +7,7 @@
# created_at :datetime
# updated_at :datetime
# key :text
+# public :boolean default(FALSE)
# title :string(255)
# type :string(255)
# fingerprint :string(255)
@@ -17,4 +18,21 @@ class DeployKey < Key
has_many :projects, through: :deploy_keys_projects
scope :in_projects, ->(projects) { joins(:deploy_keys_projects).where('deploy_keys_projects.project_id in (?)', projects) }
+ scope :are_public, -> { where(public: true) }
+
+ def private?
+ !public?
+ end
+
+ def orphaned?
+ self.deploy_keys_projects.length == 0
+ end
+
+ def almost_orphaned?
+ self.deploy_keys_projects.length == 1
+ end
+
+ def destroyed_when_orphaned?
+ self.private?
+ end
end
diff --git a/app/models/deploy_keys_project.rb b/app/models/deploy_keys_project.rb
index 7e88903b9af..18db521741f 100644
--- a/app/models/deploy_keys_project.rb
+++ b/app/models/deploy_keys_project.rb
@@ -22,6 +22,8 @@ class DeployKeysProject < ActiveRecord::Base
private
def destroy_orphaned_deploy_key
- self.deploy_key.destroy if self.deploy_key.deploy_keys_projects.length == 0
+ return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned?
+
+ self.deploy_key.destroy
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 57f6d5cd4e0..c9a88ffa8e0 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -183,7 +183,11 @@ class Event < ActiveRecord::Base
elsif commented?
"commented on"
elsif created_project?
- "created"
+ if project.import?
+ "imported"
+ else
+ "created"
+ end
else
"opened"
end
diff --git a/app/models/group.rb b/app/models/group.rb
index da9621a2a1a..1386a9eccc9 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -46,19 +46,18 @@ class Group < Namespace
@owners ||= group_members.owners.map(&:user)
end
- def add_users(user_ids, access_level)
- user_ids.compact.each do |user_id|
- user = self.group_members.find_or_initialize_by(user_id: user_id)
- user.update_attributes(access_level: access_level)
+ def add_users(user_ids, access_level, current_user = nil)
+ user_ids.each do |user_id|
+ Member.add_user(self.group_members, user_id, access_level, current_user)
end
end
- def add_user(user, access_level)
- self.group_members.create(user_id: user.id, access_level: access_level)
+ def add_user(user, access_level, current_user = nil)
+ add_users([user], access_level, current_user)
end
- def add_owner(user)
- self.add_user(user, Gitlab::Access::OWNER)
+ def add_owner(user, current_user = nil)
+ self.add_user(user, Gitlab::Access::OWNER, current_user)
end
def has_owner?(user)
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index defef7216f2..315d96af1b9 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -28,7 +28,7 @@ class WebHook < ActiveRecord::Base
default_timeout Gitlab.config.gitlab.webhook_timeout
validates :url, presence: true,
- format: { with: URI::regexp(%w(http https)), message: "should be a valid url" }
+ format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }
def execute(data)
parsed_url = URI.parse(url)
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 440fcd0d052..756d19adec7 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -15,4 +15,5 @@ class Identity < ActiveRecord::Base
belongs_to :user
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider }
+ validates :user_id, uniqueness: { scope: :provider }
end
diff --git a/app/models/key.rb b/app/models/key.rb
index e2e59296eed..016eee86992 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -16,7 +16,6 @@ require 'digest/md5'
class Key < ActiveRecord::Base
include Sortable
- include Gitlab::Popen
belongs_to :user
@@ -79,20 +78,9 @@ class Key < ActiveRecord::Base
def generate_fingerprint
self.fingerprint = nil
- return unless key.present?
-
- cmd_status = 0
- cmd_output = ''
- Tempfile.open('gitlab_key_file') do |file|
- file.puts key
- file.rewind
- cmd_output, cmd_status = popen(%W(ssh-keygen -lf #{file.path}), '/tmp')
- end
-
- if cmd_status.zero?
- cmd_output.gsub /(\h{2}:)+\h{2}/ do |match|
- self.fingerprint = match
- end
- end
+
+ return unless self.key.present?
+
+ self.fingerprint = Gitlab::KeyFingerprint.new(self.key).fingerprint
end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index fe3d2f40e87..d151c7b2390 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -11,6 +11,10 @@
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string
+# invite_token :string
+# invite_accepted_at :datetime
#
class Member < ActiveRecord::Base
@@ -18,19 +22,151 @@ class Member < ActiveRecord::Base
include Notifiable
include Gitlab::Access
+ attr_accessor :raw_invite_token
+
+ belongs_to :created_by, class_name: "User"
belongs_to :user
belongs_to :source, polymorphic: true
- validates :user, presence: true
+ validates :user, presence: true, unless: :invite?
validates :source, presence: true
- validates :user_id, uniqueness: { scope: [:source_type, :source_id], message: "already exists in source" }
+ validates :user_id, uniqueness: { scope: [:source_type, :source_id],
+ message: "already exists in source",
+ allow_nil: true }
validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true
+ validates :invite_email, presence: { if: :invite? },
+ email: { strict_mode: true, allow_nil: true },
+ uniqueness: { scope: [:source_type, :source_id], allow_nil: true }
+ scope :invite, -> { where(user_id: nil) }
+ scope :non_invite, -> { where("user_id IS NOT NULL") }
scope :guests, -> { where(access_level: GUEST) }
scope :reporters, -> { where(access_level: REPORTER) }
scope :developers, -> { where(access_level: DEVELOPER) }
scope :masters, -> { where(access_level: MASTER) }
scope :owners, -> { where(access_level: OWNER) }
+ before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
+ after_create :send_invite, if: :invite?
+ after_create :post_create_hook, unless: :invite?
+ after_update :post_update_hook, unless: :invite?
+ after_destroy :post_destroy_hook, unless: :invite?
+
delegate :name, :username, :email, to: :user, prefix: true
+
+ class << self
+ def find_by_invite_token(invite_token)
+ invite_token = Devise.token_generator.digest(self, :invite_token, invite_token)
+ find_by(invite_token: invite_token)
+ end
+
+ # This method is used to find users that have been entered into the "Add members" field.
+ # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
+ def user_for_id(user_id)
+ return user_id if user_id.is_a?(User)
+
+ user = User.find_by(id: user_id)
+ user ||= User.find_by(email: user_id)
+ user ||= user_id
+ user
+ end
+
+ def add_user(members, user_id, access_level, current_user = nil)
+ user = user_for_id(user_id)
+
+ # `user` can be either a User object or an email to be invited
+ if user.is_a?(User)
+ member = members.find_or_initialize_by(user_id: user.id)
+ else
+ member = members.build
+ member.invite_email = user
+ end
+
+ member.created_by ||= current_user
+ member.access_level = access_level
+
+ member.save
+ end
+ end
+
+ def invite?
+ self.invite_token.present?
+ end
+
+ def accept_invite!(new_user)
+ return false unless invite?
+
+ self.invite_token = nil
+ self.invite_accepted_at = Time.now.utc
+
+ self.user = new_user
+
+ saved = self.save
+
+ after_accept_invite if saved
+
+ saved
+ end
+
+ def decline_invite!
+ return false unless invite?
+
+ destroyed = self.destroy
+
+ after_decline_invite if destroyed
+
+ destroyed
+ end
+
+ def generate_invite_token
+ raw, enc = Devise.token_generator.generate(self.class, :invite_token)
+ @raw_invite_token = raw
+ self.invite_token = enc
+ end
+
+ def generate_invite_token!
+ generate_invite_token && save(validate: false)
+ end
+
+ def resend_invite
+ return unless invite?
+
+ generate_invite_token! unless @raw_invite_token
+
+ send_invite
+ end
+
+ private
+
+ def send_invite
+ # override in subclass
+ end
+
+ def post_create_hook
+ system_hook_service.execute_hooks_for(self, :create)
+ end
+
+ def post_update_hook
+ # override in subclass
+ end
+
+ def post_destroy_hook
+ system_hook_service.execute_hooks_for(self, :destroy)
+ end
+
+ def after_accept_invite
+ post_create_hook
+ end
+
+ def after_decline_invite
+ # override in subclass
+ end
+
+ def system_hook_service
+ SystemHooksService.new
+ end
+
+ def notification_service
+ NotificationService.new
+ end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 28d0b4483b4..84c91372b3f 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -27,10 +27,6 @@ class GroupMember < Member
scope :with_group, ->(group) { where(source_id: group.id) }
scope :with_user, ->(user) { where(user_id: user.id) }
- after_create :post_create_hook
- after_update :notify_update
- after_destroy :post_destroy_hook
-
def self.access_level_roles
Gitlab::Access.options_with_owner
end
@@ -43,26 +39,37 @@ class GroupMember < Member
access_level
end
+ private
+
+ def send_invite
+ notification_service.invite_group_member(self, @raw_invite_token)
+
+ super
+ end
+
def post_create_hook
notification_service.new_group_member(self)
- system_hook_service.execute_hooks_for(self, :create)
+
+ super
end
- def notify_update
+ def post_update_hook
if access_level_changed?
notification_service.update_group_member(self)
end
- end
- def post_destroy_hook
- system_hook_service.execute_hooks_for(self, :destroy)
+ super
end
- def system_hook_service
- SystemHooksService.new
+ def after_accept_invite
+ notification_service.accept_group_invite(self)
+
+ super
end
- def notification_service
- NotificationService.new
+ def after_decline_invite
+ notification_service.decline_group_invite(self)
+
+ super
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 6b13e0ff30b..0a3b4d2182b 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -27,10 +27,6 @@ class ProjectMember < Member
validates_format_of :source_type, with: /\AProject\z/
default_scope { where(source_type: SOURCE_TYPE) }
- after_create :post_create_hook
- after_update :post_update_hook
- after_destroy :post_destroy_hook
-
scope :in_project, ->(project) { where(source_id: project.id) }
scope :in_projects, ->(projects) { where(source_id: projects.pluck(:id)) }
scope :with_user, ->(user) { where(user_id: user.id) }
@@ -55,7 +51,7 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_into_projects(project_ids, user_ids, access)
+ def add_users_into_projects(project_ids, user_ids, access, current_user = nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -64,12 +60,14 @@ class ProjectMember < Member
raise "Non valid access"
end
+ users = user_ids.map { |user_id| Member.user_for_id(user_id) }
+
ProjectMember.transaction do
project_ids.each do |project_id|
- user_ids.each do |user_id|
- member = ProjectMember.new(access_level: access_level, user_id: user_id)
- member.source_id = project_id
- member.save
+ project = Project.find(project_id)
+
+ users.each do |user|
+ Member.add_user(project.project_members, user, access_level, current_user)
end
end
end
@@ -82,6 +80,7 @@ class ProjectMember < Member
def truncate_teams(project_ids)
ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids)
+
members.each do |member|
member.destroy
end
@@ -109,41 +108,58 @@ class ProjectMember < Member
access_level
end
+ def project
+ source
+ end
+
def owner?
project.owner == user
end
+ private
+
+ def send_invite
+ notification_service.invite_project_member(self, @raw_invite_token)
+
+ super
+ end
+
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
notification_service.new_project_member(self)
end
- system_hook_service.execute_hooks_for(self, :create)
+ super
end
def post_update_hook
- notification_service.update_project_member(self) if self.access_level_changed?
+ if access_level_changed?
+ notification_service.update_project_member(self)
+ end
+
+ super
end
def post_destroy_hook
event_service.leave_project(self.project, self.user)
- system_hook_service.execute_hooks_for(self, :destroy)
- end
- def event_service
- EventCreateService.new
+ super
end
- def notification_service
- NotificationService.new
+ def after_accept_invite
+ notification_service.accept_project_invite(self)
+
+ super
end
- def system_hook_service
- SystemHooksService.new
+ def after_decline_invite
+ notification_service.decline_project_invite(self)
+
+ super
end
- def project
- source
+ def event_service
+ EventCreateService.new
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5634f9a686e..9c9e2762507 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -257,11 +257,11 @@ class MergeRequest < ActiveRecord::Base
end
# Return the set of issues that will be closed if this merge request is accepted.
- def closes_issues
+ def closes_issues(current_user = self.author)
if target_branch == project.default_branch
- issues = commits.flat_map { |c| c.closes_issues(project) }
- issues.push(*Gitlab::ClosingIssueExtractor.
- closed_by_message_in_project(description, project))
+ issues = commits.flat_map { |c| c.closes_issues(project, current_user) }
+ issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
+ closed_by_message(description))
issues.uniq.sort_by(&:id)
else
[]
@@ -361,6 +361,8 @@ class MergeRequest < ActiveRecord::Base
end
def locked_long_ago?
- locked_at && locked_at < (Time.now - 1.day)
+ return false unless locked?
+
+ locked_at.nil? || locked_at < (Time.now - 1.day)
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 35280889a86..e1de114375e 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -24,8 +24,8 @@ class Namespace < ActiveRecord::Base
validates :name,
presence: true, uniqueness: true,
length: { within: 0..255 },
- format: { with: Gitlab::Regex.name_regex,
- message: Gitlab::Regex.name_regex_message }
+ format: { with: Gitlab::Regex.namespace_name_regex,
+ message: Gitlab::Regex.namespace_name_regex_message }
validates :description, length: { within: 0..255 }
validates :path,
@@ -33,8 +33,8 @@ class Namespace < ActiveRecord::Base
presence: true,
length: { within: 1..255 },
exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.path_regex,
- message: Gitlab::Regex.path_regex_message }
+ format: { with: Gitlab::Regex.namespace_regex,
+ message: Gitlab::Regex.namespace_regex_message }
delegate :name, to: :owner, allow_nil: true, prefix: true
@@ -44,21 +44,37 @@ class Namespace < ActiveRecord::Base
scope :root, -> { where('type IS NULL') }
- def self.by_path(path)
- where('lower(path) = :value', value: path.downcase).first
- end
+ class << self
+ def by_path(path)
+ where('lower(path) = :value', value: path.downcase).first
+ end
- # Case insensetive search for namespace by path or name
- def self.find_by_path_or_name(path)
- find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
- end
+ # Case insensetive search for namespace by path or name
+ def find_by_path_or_name(path)
+ find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
+ end
- def self.search(query)
- where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
- end
+ def search(query)
+ where("name LIKE :query OR path LIKE :query", query: "%#{query}%")
+ end
+
+ def clean_path(path)
+ path = path.dup
+ path.gsub!(/@.*\z/, "")
+ path.gsub!(/\.git\z/, "")
+ path.gsub!(/\A-+/, "")
+ path.gsub!(/\.+\z/, "")
+ path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+
+ counter = 0
+ base = path
+ while Namespace.by_path(path).present?
+ counter += 1
+ path = "#{base}#{counter}"
+ end
- def self.global_id
- 'GLN'
+ path
+ end
end
def to_param
diff --git a/app/models/note.rb b/app/models/note.rb
index e86160e7cd9..2cf3fac2def 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -22,6 +22,7 @@ require 'file_size_validator'
class Note < ActiveRecord::Base
include Mentionable
+ include Gitlab::CurrentSettings
default_value_for :system, false
@@ -36,7 +37,8 @@ class Note < ActiveRecord::Base
validates :note, :project, presence: true
validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
- validates :attachment, file_size: { maximum: 10.megabytes.to_i }
+ # Attachments are deprecated and are handled by Markdown uploader
+ validates :attachment, file_size: { maximum: :max_attachment_size }
validates :noteable_id, presence: true, if: ->(n) { n.noteable_type.present? && n.noteable_type != 'Commit' }
validates :commit_id, presence: true, if: ->(n) { n.noteable_type == 'Commit' }
@@ -321,6 +323,10 @@ class Note < ActiveRecord::Base
end
end
+ def max_attachment_size
+ current_application_settings.max_attachment_size.megabytes.to_i
+ end
+
def commit_author
@commit_author ||=
project.team.users.find_by(email: noteable.author_email) ||
@@ -348,7 +354,7 @@ class Note < ActiveRecord::Base
def set_diff
# First lets find notes with same diff
# before iterating over all mr diffs
- diff = Note.where(noteable_id: self.noteable_id, noteable_type: self.noteable_type, line_code: self.line_code).last.try(:diff)
+ diff = diff_for_line_code unless for_merge_request?
diff ||= find_diff
self.st_diff = diff.to_hash if diff
@@ -358,6 +364,10 @@ class Note < ActiveRecord::Base
@diff ||= Gitlab::Git::Diff.new(st_diff) if st_diff.respond_to?(:map)
end
+ def diff_for_line_code
+ Note.where(noteable_id: noteable_id, noteable_type: noteable_type, line_code: line_code).last.try(:diff)
+ end
+
# Check if such line of code exists in merge request diff
# If exists - its active discussion
# If not - its outdated diff
@@ -451,7 +461,7 @@ class Note < ActiveRecord::Base
prev_match_line = line
else
prev_lines << line
-
+
break if generate_line_code(line) == self.line_code
prev_lines.shift if prev_lines.length >= max_number_of_lines
diff --git a/app/models/project.rb b/app/models/project.rb
index c50b8a12621..64ee2c2212b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -81,7 +81,7 @@ class Project < ActiveRecord::Base
has_one :asana_service, dependent: :destroy
has_one :gemnasium_service, dependent: :destroy
has_one :slack_service, dependent: :destroy
- has_one :buildbox_service, dependent: :destroy
+ has_one :buildkite_service, dependent: :destroy
has_one :bamboo_service, dependent: :destroy
has_one :teamcity_service, dependent: :destroy
has_one :pushover_service, dependent: :destroy
@@ -114,6 +114,8 @@ class Project < ActiveRecord::Base
has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user
+ has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
+
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -124,12 +126,12 @@ class Project < ActiveRecord::Base
presence: true,
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_name_regex,
- message: Gitlab::Regex.project_regex_message }
+ message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
length: { within: 0..255 },
- format: { with: Gitlab::Regex.path_regex,
- message: Gitlab::Regex.path_regex_message }
+ format: { with: Gitlab::Regex.project_path_regex,
+ message: Gitlab::Regex.project_path_regex_message }
validates :issues_enabled, :merge_requests_enabled,
:wiki_enabled, inclusion: { in: [true, false] }
validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
@@ -137,7 +139,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
- format: { with: URI::regexp(%w(ssh git http https)), message: 'should be a valid url' },
+ format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' },
if: :import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
@@ -185,6 +187,7 @@ class Project < ActiveRecord::Base
state :failed
after_transition any => :started, do: :add_import_job
+ after_transition any => :finished, do: :clear_import_data
end
class << self
@@ -262,6 +265,10 @@ class Project < ActiveRecord::Base
RepositoryImportWorker.perform_in(2.seconds, id)
end
+ def clear_import_data
+ self.import_data.destroy if self.import_data
+ end
+
def import?
import_url.present?
end
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
new file mode 100644
index 00000000000..6a8a8a56eb5
--- /dev/null
+++ b/app/models/project_import_data.rb
@@ -0,0 +1,19 @@
+# == Schema Information
+#
+# Table name: project_import_datas
+#
+# id :integer not null, primary key
+# project_id :integer
+# data :text
+#
+
+require 'carrierwave/orm/activerecord'
+require 'file_size_validator'
+
+class ProjectImportData < ActiveRecord::Base
+ belongs_to :project
+
+ serialize :data, JSON
+
+ validates :project, presence: true
+end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index f968afe9fa8..d8aedbd2ab4 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -25,7 +25,7 @@ class BambooService < CiService
validates :bamboo_url,
presence: true,
- format: { with: URI::regexp },
+ format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
validates :build_key, presence: true, if: :activated?
validates :username,
diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildkite_service.rb
index 3a381ff11b8..a714bc82246 100644
--- a/app/models/project_services/buildbox_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -20,9 +20,7 @@
require "addressable/uri"
-# Buildbox renamed to Buildkite, but for backwards compatability with the STI
-# of Services, the class name is kept as "Buildbox"
-class BuildboxService < CiService
+class BuildkiteService < CiService
ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index e521186798c..a199d0e86f2 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -18,7 +18,7 @@ class ExternalWikiService < Service
prop_accessor :external_wiki_url
validates :external_wiki_url,
presence: true,
- format: { with: URI::regexp },
+ format: { with: /\A#{URI.regexp}\z/ },
if: :activated?
def title
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index edaeeffc228..0f9838a575d 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -18,6 +18,8 @@
#
class GitlabCiService < CiService
+ API_PREFIX = "api/v1"
+
prop_accessor :project_url, :token
validates :project_url, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -59,6 +61,26 @@ class GitlabCiService < CiService
end
end
+ def fork_registration(new_project, private_token)
+ params = {
+ id: new_project.id,
+ name_with_namespace: new_project.name_with_namespace,
+ web_url: new_project.web_url,
+ default_branch: new_project.default_branch,
+ ssh_url_to_repo: new_project.ssh_url_to_repo
+ }
+
+ HTTParty.post(
+ fork_registration_path,
+ body: {
+ project_id: project.id,
+ project_token: token,
+ private_token: private_token,
+ data: params },
+ verify: false
+ )
+ end
+
def commit_coverage(sha, ref)
response = get_ci_build(sha, ref)
@@ -97,4 +119,10 @@ class GitlabCiService < CiService
{ type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3' }
]
end
+
+ private
+
+ def fork_registration_path
+ project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks")
+ end
end
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 84346350a6c..5f0553f3b0b 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -20,8 +20,13 @@
class GitlabIssueTrackerService < IssueTrackerService
include Rails.application.routes.url_helpers
- prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+ default_url_options[:host] = Gitlab.config.gitlab.host
+ default_url_options[:protocol] = Gitlab.config.gitlab.protocol
+ default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
+ default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
+
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def default?
true
@@ -32,20 +37,26 @@ class GitlabIssueTrackerService < IssueTrackerService
end
def project_url
- "#{gitlab_url}#{namespace_project_issues_path(project.namespace, project)}"
+ namespace_project_issues_url(project.namespace, project)
end
def new_issue_url
- "#{gitlab_url}#{new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project)}"
+ new_namespace_project_issue_url(namespace_id: project.namespace, project_id: project)
end
def issue_url(iid)
- "#{gitlab_url}#{namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)}"
+ namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: iid)
end
- private
+ def project_path
+ namespace_project_issues_path(project.namespace, project)
+ end
+
+ def new_issue_path
+ new_namespace_project_issue_path(namespace_id: project.namespace, project_id: project)
+ end
- def gitlab_url
- Gitlab.config.gitlab.relative_url_root.chomp("/") if Gitlab.config.gitlab.relative_url_root
+ def issue_path(iid)
+ namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: iid)
end
end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index 2bddb7b881c..e9e1e276e7d 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -148,7 +148,7 @@ class IrkerService < Service
def consider_uri(uri)
# Authorize both irc://domain.com/#chan and irc://domain.com/chan
- if uri.is_a?(URI) && uri.scheme[/^ircs?$/] && !uri.path.nil?
+ if uri.is_a?(URI) && uri.scheme[/^ircs?\z/] && !uri.path.nil?
# Do not authorize irc://domain.com/
if uri.fragment.nil? && uri.path.length > 1
uri.to_s
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 8e90c44d103..c8ab9d63b74 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -34,6 +34,18 @@ class IssueTrackerService < Service
self.issues_url.gsub(':id', iid.to_s)
end
+ def project_path
+ project_url
+ end
+
+ def new_issue_path
+ new_issue_url
+ end
+
+ def issue_path(iid)
+ issue_url(iid)
+ end
+
def fields
[
{ type: 'text', name: 'description', placeholder: description },
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index c26bc551352..3c002a1634b 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -25,7 +25,7 @@ class TeamcityService < CiService
validates :teamcity_url,
presence: true,
- format: { with: URI::regexp }, if: :activated?
+ format: { with: /\A#{URI.regexp}\z/ }, if: :activated?
validates :build_type, presence: true, if: :activated?
validates :username,
presence: true,
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d4a07caf9ef..56e49af2324 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -12,12 +12,12 @@ class ProjectTeam
# @team << [@users, :master]
#
def <<(args)
- users = args.first
+ users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, args.second)
+ add_users(users, access, current_user)
else
- add_user(users, args.second)
+ add_user(users, access, current_user)
end
end
@@ -43,22 +43,19 @@ class ProjectTeam
member
end
- def add_user(user, access)
- add_users_ids([user.id], access)
- end
-
- def add_users(users, access)
- add_users_ids(users.map(&:id), access)
- end
-
- def add_users_ids(user_ids, access)
+ def add_users(users, access, current_user = nil)
ProjectMember.add_users_into_projects(
[project.id],
- user_ids,
- access
+ users,
+ access,
+ current_user
)
end
+ def add_user(user, access, current_user = nil)
+ add_users([user], access, current_user)
+ end
+
# Remove all users from project team
def truncate
ProjectMember.truncate_team(project)
@@ -88,7 +85,7 @@ class ProjectTeam
@masters ||= fetch_members(:masters)
end
- def import(source_project)
+ def import(source_project, current_user = nil)
target_project = project
source_members = source_project.project_members.to_a
@@ -96,13 +93,14 @@ class ProjectTeam
source_members.reject! do |member|
# Skip if user already present in team
- target_user_ids.include?(member.user_id)
+ !member.invite? && target_user_ids.include?(member.user_id)
end
source_members.map! do |member|
new_member = member.dup
new_member.id = nil
new_member.source = target_project
+ new_member.created_by = current_user
new_member
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 77765cae1a0..263a436d521 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -199,7 +199,7 @@ class Repository
def changelog
cache.fetch(:changelog) do
tree(:head).blobs.find do |file|
- file.name =~ /^(changelog|history)/i
+ file.name =~ /\A(changelog|history)/i
end
end
end
@@ -207,7 +207,7 @@ class Repository
def license
cache.fetch(:license) do
tree(:head).blobs.find do |file|
- file.name =~ /^license/i
+ file.name =~ /\Alicense/i
end
end
end
@@ -267,6 +267,9 @@ class Repository
# Remove archives older than 2 hours
def clean_old_archives
repository_downloads_path = Gitlab.config.gitlab.repository_downloads_path
+
+ return unless File.directory?(repository_downloads_path)
+
Gitlab::Popen.popen(%W(find #{repository_downloads_path} -not -path #{repository_downloads_path} -mmin +120 -delete))
end
diff --git a/app/models/service.rb b/app/models/service.rb
index f54ad19666b..393cf55a69f 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -122,25 +122,25 @@ class Service < ActiveRecord::Base
def self.available_services_names
%w(
- gitlab_ci
- campfire
- hipchat
- pivotaltracker
- flowdock
- assembla
asana
+ assembla
+ bamboo
+ buildkite
+ campfire
+ custom_issue_tracker
emails_on_push
+ external_wiki
+ flowdock
gemnasium
- slack
- pushover
- buildbox
- bamboo
- teamcity
+ gitlab_ci
+ hipchat
+ irker
jira
+ pivotaltracker
+ pushover
redmine
- custom_issue_tracker
- irker
- external_wiki
+ slack
+ teamcity
)
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 3fb2ec1d66c..b35e72c4bdb 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -33,8 +33,8 @@ class Snippet < ActiveRecord::Base
validates :file_name,
presence: true,
length: { within: 0..255 },
- format: { with: Gitlab::Regex.path_regex,
- message: Gitlab::Regex.path_regex_message }
+ format: { with: Gitlab::Regex.file_name_regex,
+ message: Gitlab::Regex.file_name_regex_message }
validates :content, presence: true
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
diff --git a/app/models/user.rb b/app/models/user.rb
index 979150b4d68..d6b93afe739 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -49,6 +49,7 @@
# password_automatically_set :boolean default(FALSE)
# bitbucket_access_token :string(255)
# bitbucket_access_token_secret :string(255)
+# public_email :string(255) default(""), not null
#
require 'carrierwave/orm/activerecord'
@@ -123,14 +124,15 @@ class User < ActiveRecord::Base
validates :name, presence: true
validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
validates :notification_email, presence: true, email: { strict_mode: true }
+ validates :public_email, presence: true, email: { strict_mode: true }, allow_blank: true, uniqueness: true
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :username,
presence: true,
uniqueness: { case_sensitive: false },
exclusion: { in: Gitlab::Blacklist.path },
- format: { with: Gitlab::Regex.username_regex,
- message: Gitlab::Regex.username_regex_message }
+ format: { with: Gitlab::Regex.namespace_regex,
+ message: Gitlab::Regex.namespace_regex_message }
validates :notification_level, inclusion: { in: Notification.notification_levels }, presence: true
validate :namespace_uniq, if: ->(user) { user.username_changed? }
@@ -142,6 +144,7 @@ class User < ActiveRecord::Base
before_validation :generate_password, on: :create
before_validation :sanitize_attrs
before_validation :set_notification_email, if: ->(user) { user.email_changed? }
+ before_validation :set_public_email, if: ->(user) { user.public_email_changed? }
before_save :ensure_authentication_token
after_save :ensure_namespace_correct
@@ -229,22 +232,6 @@ class User < ActiveRecord::Base
def build_user(attrs = {})
User.new(attrs)
end
-
- def clean_username(username)
- username.gsub!(/@.*\z/, "")
- username.gsub!(/\.git\z/, "")
- username.gsub!(/\A-/, "")
- username.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
-
- counter = 0
- base = username
- while User.by_login(username).present? || Namespace.by_path(username).present?
- counter += 1
- username = "#{base}#{counter}"
- end
-
- username
- end
end
#
@@ -430,8 +417,16 @@ class User < ActiveRecord::Base
@ldap_identity ||= identities.find_by(["provider LIKE ?", "ldap%"])
end
+ def project_deploy_keys
+ DeployKey.in_projects(self.authorized_projects.pluck(:id))
+ end
+
def accessible_deploy_keys
- DeployKey.in_projects(self.authorized_projects.pluck(:id)).uniq
+ @accessible_deploy_keys ||= begin
+ key_ids = project_deploy_keys.pluck(:id)
+ key_ids.push(*DeployKey.are_public.pluck(:id))
+ DeployKey.where(id: key_ids)
+ end
end
def created_by
@@ -451,6 +446,12 @@ class User < ActiveRecord::Base
end
end
+ def set_public_email
+ if self.public_email.blank? || !self.all_emails.include?(self.public_email)
+ self.public_email = ''
+ end
+ end
+
def set_projects_limit
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined
@@ -502,13 +503,13 @@ class User < ActiveRecord::Base
end
def full_website_url
- return "http://#{website_url}" if website_url !~ /^https?:\/\//
+ return "http://#{website_url}" if website_url !~ /\Ahttps?:\/\//
website_url
end
def short_website_url
- website_url.gsub(/https?:\/\//, '')
+ website_url.sub(/\Ahttps?:\/\//, '')
end
def all_ssh_keys
diff --git a/app/services/archive_repository_service.rb b/app/services/archive_repository_service.rb
index 8823f6fdc67..e1b41527d8d 100644
--- a/app/services/archive_repository_service.rb
+++ b/app/services/archive_repository_service.rb
@@ -1,14 +1,62 @@
class ArchiveRepositoryService
- def execute(project, ref, format)
- storage_path = Gitlab.config.gitlab.repository_downloads_path
+ attr_reader :project, :ref, :format
- unless File.directory?(storage_path)
- FileUtils.mkdir_p(storage_path)
+ def initialize(project, ref, format)
+ format ||= 'tar.gz'
+ @project, @ref, @format = project, ref, format.downcase
+ end
+
+ def execute(options = {})
+ project.repository.clean_old_archives
+
+ raise "No archive file path" unless file_path
+
+ return file_path if archived?
+
+ unless archiving?
+ RepositoryArchiveWorker.perform_async(project.id, ref, format)
end
- format ||= 'tar.gz'
- repository = project.repository
- repository.clean_old_archives
- repository.archive_repo(ref, storage_path, format.downcase)
+ archived = wait_until_archived(options[:timeout] || 5.0)
+
+ file_path if archived
+ end
+
+ private
+
+ def storage_path
+ Gitlab.config.gitlab.repository_downloads_path
+ end
+
+ def file_path
+ @file_path ||= project.repository.archive_file_path(ref, storage_path, format)
+ end
+
+ def pid_file_path
+ @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format)
+ end
+
+ def archived?
+ File.exist?(file_path)
+ end
+
+ def archiving?
+ File.exist?(pid_file_path)
+ end
+
+ def wait_until_archived(timeout = 5.0)
+ return archived? if timeout == 0.0
+
+ t1 = Time.now
+
+ begin
+ sleep 0.1
+
+ success = archived?
+
+ t2 = Time.now
+ end until success || t2 - t1 >= timeout
+
+ success
end
end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 4115d689925..25f9e203246 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -13,9 +13,7 @@ class CreateTagService < BaseService
return error('Tag already exists')
end
- if message
- message.gsub!(/^\s+|\s+$/, '')
- end
+ message.strip! if message
repository.add_tag(tag_name, ref, message)
new_tag = repository.find_tag(tag_name)
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index eeafefc25af..23833aa78ec 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -12,10 +12,10 @@ module Files
file_name = File.basename(path)
file_path = path
- unless file_name =~ Gitlab::Regex.path_regex
+ unless file_name =~ Gitlab::Regex.file_name_regex
return error(
'Your changes could not be committed, because the file name ' +
- Gitlab::Regex.path_regex_message
+ Gitlab::Regex.file_name_regex_message
)
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 1f0b29dff5e..31e0167d247 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -70,7 +70,7 @@ class GitPushService
# Close issues if these commits were pushed to the project's default branch and the commit message matches the
# closing regex. Exclude any mentioned Issues from cross-referencing even if the commits are being pushed to
# a different branch.
- issues_to_close = commit.closes_issues(project)
+ issues_to_close = commit.closes_issues(project, user)
# Load commit author only if needed.
# For push with 1k commits it prevents 900+ requests in database
@@ -87,7 +87,7 @@ class GitPushService
# Create cross-reference notes for any other references. Omit any issues that were referenced in an
# issue-closing phrase, or have already been mentioned from this commit (probably from this commit
# being pushed to a different branch).
- refs = commit.references(project) - issues_to_close
+ refs = commit.references(project, user) - issues_to_close
refs.reject! { |r| commit.has_mentioned?(r) }
if refs.present?
@@ -127,6 +127,6 @@ class GitPushService
end
def commit_user(commit)
- User.find_for_commit(commit.author_email, commit.author_name) || user
+ commit.author || user
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 3371fe7d5ef..8f04a69287a 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -14,8 +14,8 @@ module Issues
issue.update_nth_task(params[:task_num].to_i, false)
end
- params[:assignee_id] = "" if params[:assignee_id] == "-1"
- params[:milestone_id] = "" if params[:milestone_id] == "-1"
+ params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
+ params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
old_labels = issue.labels.to_a
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 0ac6dfea6fd..23af2656c37 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -23,8 +23,8 @@ module MergeRequests
merge_request.update_nth_task(params[:task_num].to_i, false)
end
- params[:assignee_id] = "" if params[:assignee_id] == "-1"
- params[:milestone_id] = "" if params[:milestone_id] == "-1"
+ params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
+ params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
old_labels = merge_request.labels.to_a
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index cc5853144c5..cfed7964c37 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -123,30 +123,30 @@ class NotificationService
return true if note.note.start_with?('Status changed to closed')
return true if note.cross_reference? && note.system == true
- opts = { noteable_type: note.noteable_type, project_id: note.project_id }
-
target = note.noteable
- if target.respond_to?(:participants)
- recipients = target.participants
- else
- recipients = note.mentioned_users
- end
+ recipients = []
if note.commit_id.present?
- opts.merge!(commit_id: note.commit_id)
recipients << note.commit_author
- else
- opts.merge!(noteable_id: note.noteable_id)
end
-
- # Get users who left comment in thread
- recipients = recipients.concat(User.where(id: Note.where(opts).pluck(:author_id)))
+
+ # Add all users participating in the thread (author, assignee, comment authors)
+ participants =
+ if target.respond_to?(:participants)
+ target.participants
+ elsif target.is_a?(Commit)
+ author_ids = Note.for_commit_id(target.id).pluck(:author_id).uniq
+ User.where(id: author_ids)
+ else
+ note.mentioned_users
+ end
+ recipients = recipients.concat(participants)
# Merge project watchers
recipients = recipients.concat(project_watchers(note.project)).compact.uniq
- # Reject mention users unless mentioned in comment
+ # Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - note.mentioned_users, note.project)
recipients = recipients + note.mentioned_users
@@ -168,6 +168,18 @@ class NotificationService
end
end
+ def invite_project_member(project_member, token)
+ mailer.project_member_invited_email(project_member.id, token)
+ end
+
+ def accept_project_invite(project_member)
+ mailer.project_invite_accepted_email(project_member.id)
+ end
+
+ def decline_project_invite(project_member)
+ mailer.project_invite_declined_email(project_member.project.id, project_member.invite_email, project_member.access_level, project_member.created_by_id)
+ end
+
def new_project_member(project_member)
mailer.project_access_granted_email(project_member.id)
end
@@ -176,6 +188,18 @@ class NotificationService
mailer.project_access_granted_email(project_member.id)
end
+ def invite_group_member(group_member, token)
+ mailer.group_member_invited_email(group_member.id, token)
+ end
+
+ def accept_group_invite(group_member)
+ mailer.group_invite_accepted_email(group_member.id)
+ end
+
+ def decline_group_invite(group_member)
+ mailer.group_invite_declined_email(group_member.group.id, group_member.invite_email, group_member.access_level, group_member.created_by_id)
+ end
+
def new_group_member(group_member)
mailer.group_access_granted_email(group_member.id)
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 7ffd0b3882a..a7afcf8f64b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -83,7 +83,7 @@ module Projects
system_hook_service.execute_hooks_for(@project, :create)
unless @project.group
- @project.team << [current_user, :master]
+ @project.team << [current_user, :master, current_user]
end
@project.update_column(:last_activity_at, @project.created_at)
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 6b0d4aca3e1..1e4deb6ed39 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -38,14 +38,20 @@ module Projects
#First save the DB entries as they can be rolled back if the repo fork fails
project.build_forked_project_link(forked_to_project_id: project.id, forked_from_project_id: @from_project.id)
if project.save
- project.team << [@current_user, :master]
+ project.team << [@current_user, :master, @current_user]
end
+
#Now fork the repo
unless gitlab_shell.fork_repository(@from_project.path_with_namespace, project.namespace.path)
raise 'forking failed in gitlab-shell'
end
+
project.ensure_satellite_exists
end
+
+ if @from_project.gitlab_ci?
+ ForkRegistrationWorker.perform_async(@from_project.id, project.id, @current_user.private_token)
+ end
rescue => ex
project.errors.add(:base, 'Fork transaction failed.')
project.destroy
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index bcbacbff562..ae6260bcdab 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -1,10 +1,5 @@
module Projects
class ParticipantsService < BaseService
- def initialize(project, user)
- @project = project
- @user = user
- end
-
def execute(note_type, note_id)
participating =
if note_type && note_id
@@ -12,7 +7,7 @@ module Projects
else
[]
end
- project_members = sorted(@project.team.members)
+ project_members = sorted(project.team.members)
participants = all_members + groups + project_members + participating
participants.uniq
end
@@ -20,11 +15,11 @@ module Projects
def participants_in(type, id)
users = case type
when "Issue"
- issue = @project.issues.find_by_iid(id)
- issue ? issue.participants : []
+ issue = project.issues.find_by_iid(id)
+ issue ? issue.participants(current_user) : []
when "MergeRequest"
- merge_request = @project.merge_requests.find_by_iid(id)
- merge_request ? merge_request.participants : []
+ merge_request = project.merge_requests.find_by_iid(id)
+ merge_request ? merge_request.participants(current_user) : []
when "Commit"
author_ids = Note.for_commit_id(id).pluck(:author_id).uniq
User.where(id: author_ids)
@@ -41,14 +36,14 @@ module Projects
end
def groups
- @user.authorized_groups.sort_by(&:path).map do |group|
+ current_user.authorized_groups.sort_by(&:path).map do |group|
count = group.users.count
{ username: group.path, name: "#{group.name} (#{count})" }
end
end
def all_members
- count = @project.team.members.flatten.count
+ count = project.team.members.flatten.count
[{ username: "all", name: "All Project and Group Members (#{count})" }]
end
end
diff --git a/app/services/projects/upload_service.rb b/app/services/projects/upload_service.rb
index a186c97628f..992a7a7a1dc 100644
--- a/app/services/projects/upload_service.rb
+++ b/app/services/projects/upload_service.rb
@@ -5,7 +5,7 @@ module Projects
end
def execute
- return nil unless @file
+ return nil unless @file and @file.size <= max_attachment_size
uploader = FileUploader.new(@project)
uploader.store!(@file)
@@ -18,5 +18,11 @@ module Projects
'is_image' => uploader.image?
}
end
+
+ private
+
+ def max_attachment_size
+ current_application_settings.max_attachment_size.megabytes.to_i
+ end
end
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index edfcccfcf4c..4f3565c67eb 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -60,5 +60,10 @@
.col-sm-10
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled
+ .form-group
+ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :max_attachment_size, class: 'form-control'
+
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index b77d188a38d..fa4e6335c73 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -1,13 +1,15 @@
= form_for [:admin, @application], url: @url, html: {class: 'form-horizontal', role: 'form'} do |f|
- if application.errors.any?
- .alert.alert-danger{"data-alert" => ""}
- %p Whoops! Check your form for possible errors
- = content_tag :div, class: "form-group#{' has-error' if application.errors[:name].present?}" do
+ .alert.alert-danger
+ %button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
+ - application.errors.full_messages.each do |msg|
+ %p= msg
+ = content_tag :div, class: 'form-group' do
= f.label :name, class: 'col-sm-2 control-label'
.col-sm-10
= f.text_field :name, class: 'form-control'
= doorkeeper_errors_for application, :name
- = content_tag :div, class: "form-group#{' has-error' if application.errors[:redirect_uri].present?}" do
+ = content_tag :div, class: 'form-group' do
= f.label :redirect_uri, class: 'col-sm-2 control-label'
.col-sm-10
= f.text_area :redirect_uri, class: 'form-control'
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 8db2b2a709c..4ef8e878a7f 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -41,4 +41,4 @@
.panel.panel-default
- %iframe{src: sidekiq_path, width: '100%', height: 900, style: "border: none"}
+ %iframe{src: sidekiq_path, width: '100%', height: 970, style: "border: none"}
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
new file mode 100644
index 00000000000..2ae83ab95f7
--- /dev/null
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -0,0 +1,27 @@
+.panel.panel-default
+ .panel-heading
+ Public deploy keys (#{@deploy_keys.count})
+ .panel-head-actions
+ = link_to 'New Deploy Key', new_admin_deploy_key_path, class: "btn btn-new btn-sm"
+ - if @deploy_keys.any?
+ %table.table
+ %thead.panel-heading
+ %tr
+ %th Title
+ %th Fingerprint
+ %th Added at
+ %th
+ %tbody
+ - @deploy_keys.each do |deploy_key|
+ %tr
+ %td
+ = link_to admin_deploy_key_path(deploy_key) do
+ %strong= deploy_key.title
+ %td
+ %span
+ (#{deploy_key.fingerprint})
+ %td
+ %span.cgray
+ added #{time_ago_with_tooltip(deploy_key.created_at)}
+ %td
+ = link_to 'Remove', admin_deploy_key_path(deploy_key), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml
new file mode 100644
index 00000000000..c00049424c5
--- /dev/null
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -0,0 +1,26 @@
+%h3.page-title New public deploy key
+%hr
+
+%div
+ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form form-horizontal' } do |f|
+ -if @deploy_key.errors.any?
+ .alert.alert-danger
+ %ul
+ - @deploy_key.errors.full_messages.each do |msg|
+ %li= msg
+
+ .form-group
+ = f.label :title, class: "control-label"
+ .col-sm-10= f.text_field :title, class: 'form-control'
+ .form-group
+ = f.label :key, class: "control-label"
+ .col-sm-10
+ %p.light
+ Paste a machine public key here. Read more about how to generate it
+ = link_to "here", help_page_path("ssh", "README")
+ = f.text_area :key, class: "form-control thin_area", rows: 5
+
+ .form-actions
+ = f.submit 'Create', class: "btn-create btn"
+ = link_to "Cancel", admin_deploy_keys_path, class: "btn btn-cancel"
+
diff --git a/app/views/admin/deploy_keys/show.html.haml b/app/views/admin/deploy_keys/show.html.haml
new file mode 100644
index 00000000000..cfa2adf92ee
--- /dev/null
+++ b/app/views/admin/deploy_keys/show.html.haml
@@ -0,0 +1,34 @@
+.row
+ .col-md-4
+ .panel.panel-default
+ .panel-heading
+ Deploy Key
+ %ul.well-list
+ %li
+ %span.light Title:
+ %strong= @deploy_key.title
+ %li
+ %span.light Created on:
+ %strong= @deploy_key.created_at.stamp("Aug 21, 2011")
+
+ .panel.panel-default
+ .panel-heading Projects (#{@deploy_key.deploy_keys_projects.count})
+ - if @deploy_key.deploy_keys_projects.any?
+ %ul.well-list
+ - @deploy_key.projects.each do |project|
+ %li
+ %span
+ %strong
+ = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ .pull-right
+ = link_to disable_namespace_project_deploy_key_path(project.namespace, project, @deploy_key), data: { confirm: "Are you sure?" }, method: :put, class: "btn-xs btn btn-remove", title: 'Remove deploy key from project' do
+ %i.fa.fa-times.fa-inverse
+
+ .col-md-8
+ %p
+ %span.light Fingerprint:
+ %strong= @deploy_key.fingerprint
+ %pre.well-pre
+ = @deploy_key.key
+ .pull-right
+ = link_to 'Remove', admin_deploy_key_path(@deploy_key), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key"
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 7d292118075..14996dcd6a2 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -60,7 +60,7 @@
= form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put do
%div
- = users_select_tag(:user_ids, multiple: true)
+ = users_select_tag(:user_ids, multiple: true, email_user: true)
%div.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
@@ -74,13 +74,18 @@
%ul.well-list.group-users-list
- @members.each do |member|
- user = member.user
- %li{class: dom_class(member), id: dom_id(user)}
+ %li{class: dom_class(member), id: (dom_id(user) if user)}
.list-item-name
- %strong
- = link_to user.name, admin_user_path(user)
+ - if user
+ %strong
+ = link_to user.name, admin_user_path(user)
+ - else
+ %strong
+ = member.invite_email
+ (invited)
%span.pull-right.light
= member.human_access
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
.panel-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 05372f4124f..78684c692c7 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -42,11 +42,11 @@
%li
%span.light http:
%strong
- = link_to @project.http_url_to_repo
+ = link_to @project.http_url_to_repo, project_path(@project)
%li
%span.light ssh:
%strong
- = link_to @project.ssh_url_to_repo
+ = link_to @project.ssh_url_to_repo, project_path(@project)
- if @project.repository.exists?
%li
%span.light fs:
@@ -124,14 +124,19 @@
- user = project_member.user
%li.project_member
.list-item-name
- %strong
- = link_to user.name, admin_user_path(user)
+ - if user
+ %strong
+ = link_to user.name, admin_user_path(user)
+ - else
+ %strong
+ = project_member.invite_email
+ (invited)
.pull-right
- if project_member.owner?
%span.light Owner
- else
%span.light= project_member.human_access
- = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
+ = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_from_project_team_message(@project, project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
%i.fa.fa-times
.panel-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index 0a2934d3bda..3524f04c5ed 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -182,7 +182,7 @@
.pull-right
%span.light= group_member.human_access
- unless group_member.owner?
- = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
- else
.nothing-here-block This user has no groups.
@@ -221,7 +221,7 @@
%span.light= member.human_access
- if member.respond_to? :project
- = link_to namespace_project_project_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
#ssh-keys.tab-pane
= render 'profiles/keys/key_table', admin: true
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 165db214d75..0cb7f764fab 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -17,7 +17,7 @@
- group = group_member.group
%li
.pull-right
- - if can?(current_user, :manage_group, group)
+ - if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do
%i.fa.fa-cogs
Settings
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
new file mode 100644
index 00000000000..21e730bb7ff
--- /dev/null
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -0,0 +1,20 @@
+%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
+ %h4
+ = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
+ .row
+ .col-sm-6
+ = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do
+ = pluralize milestone.issue_count, 'Issue'
+ &nbsp;
+ = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do
+ = pluralize milestone.merge_requests_count, 'Merge Request'
+ &nbsp;
+ %span.light #{milestone.percent_complete}% complete
+
+ .col-sm-6
+ = milestone_progress_bar(milestone)
+ %div
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = milestone.project.name_with_namespace
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index caf3b685864..9944c0df815 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -16,23 +16,5 @@
.nothing-here-block No milestones to show
- else
- @dashboard_milestones.each do |milestone|
- %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- %h4
- = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
- %div
- %div
- = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &nbsp;
- = link_to dashboard_milestone_path(milestone.safe_title, title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- &nbsp;
- %span.light #{milestone.percent_complete}% complete
- = milestone_progress_bar(milestone)
- %div
- %br
- - milestone.milestones.each do |milestone|
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do
- %span.label.label-default
- = milestone.project.name_with_namespace
+ = render 'milestone', milestone: milestone
= paginate @dashboard_milestones, theme: "gitlab"
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 670f5ac7af7..f4ad2b294b3 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -19,5 +19,5 @@
%i.fa.fa-angle-left
- else
- %h3 You dont have starred projects yet
+ %h3 You don't have starred projects yet
%p.slead Visit project page and press on star icon and it will appear on this page.
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index dcf60c90430..9dc6aeffd59 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -22,5 +22,6 @@
.clearfix.prepend-top-20
%p
- %span.light Did not receive confirmation email?
- = link_to "Send again", new_confirmation_path(resource_name) \ No newline at end of file
+ %span.light Didn't receive a confirmation email?
+ = succeed '.' do
+ = link_to "Request a new one", new_confirmation_path(resource_name)
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 3c7153d235f..552525f4a07 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -18,10 +18,10 @@
%a.twitter-share-button{ |
href: "https://twitter.com/share", |
"data-url" => event.project.web_url, |
- "data-text" => "I just created a new project in GitLab! GitLab is version control on your server.", |
+ "data-text" => "I just #{event.project.imported? ? "imported" : "created"} a new project in GitLab! GitLab is version control on your server.", |
"data-size" => "medium", |
"data-related" => "gitlab", |
"data-hashtags" => "gitlab", |
"data-count" => "none"}
Tweet
- %script{src: "//platform.twitter.com/widgets.js"} \ No newline at end of file
+ %script{src: "//platform.twitter.com/widgets.js"}
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index 30e5faf822e..56b1948a474 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,17 +1,32 @@
- user = member.user
-- return unless user
+- return unless user || member.invite?
- show_roles = true if show_roles.nil?
%li{class: "#{dom_class(member)} js-toggle-container", id: dom_id(member)}
%span{class: ("list-item-name" if show_controls)}
- = image_tag avatar_icon(user.email, 16), class: "avatar s16"
- %strong= user.name
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
+ - if member.user
+ = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
+ %strong= user.name
+ %span.cgray= user.username
+ - if user == current_user
+ %span.label.label-success It's you
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+ - else
+ = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ %strong
+ = member.invite_email
+ %span.cgray
+ invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if show_controls && can?(current_user, :admin_group, @group)
+ = link_to resend_invite_group_group_member_path(@group, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
+ Resend invite
- if show_roles
%span.pull-right
@@ -27,7 +42,7 @@
= link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
- else
- = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
+ = link_to group_group_member_path(@group, member), data: { confirm: remove_user_from_group_message(@group, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-minus.fa-inverse
.edit-member.hide.js-toggle-content
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 c4c29bb2e8d..3361d7e2a8d 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -1,7 +1,10 @@
= form_for @group_member, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
.form-group
= f.label :user_ids, "People", class: 'control-label'
- .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large')
+ .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.
.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 0d501fe7bd3..c0c9cd170ad 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -16,7 +16,7 @@
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' }
= button_tag 'Search', class: 'btn'
- - if current_user && current_user.can?(:manage_group, @group)
+ - if current_user && current_user.can?(:admin_group, @group)
.pull-right
= button_tag class: 'btn btn-new js-toggle-button', type: 'button' do
Add members
diff --git a/app/views/groups/milestones/_issue.html.haml b/app/views/groups/milestones/_issue.html.haml
index 27d0c62df8c..09f9b4b8969 100644
--- a/app/views/groups/milestones/_issue.html.haml
+++ b/app/views/groups/milestones/_issue.html.haml
@@ -7,4 +7,4 @@
= link_to_gfm issue.title, [project.namespace.becomes(Namespace), project, issue], title: issue.title
.pull-right.assignee-icon
- if issue.assignee
- = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16"
+ = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_merge_request.html.haml b/app/views/groups/milestones/_merge_request.html.haml
index b2d2097dfab..d0d1426762b 100644
--- a/app/views/groups/milestones/_merge_request.html.haml
+++ b/app/views/groups/milestones/_merge_request.html.haml
@@ -7,4 +7,4 @@
= link_to_gfm merge_request.title, [project.namespace.becomes(Namespace), project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16"
+ = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: ''
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
new file mode 100644
index 00000000000..30093d2d05d
--- /dev/null
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -0,0 +1,25 @@
+%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
+ .pull-right
+ - if can?(current_user, :admin_group, @group)
+ - if milestone.closed?
+ = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
+ - else
+ = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
+ %h4
+ = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
+ .row
+ .col-sm-6
+ = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
+ = pluralize milestone.issue_count, 'Issue'
+ &nbsp;
+ = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
+ = pluralize milestone.merge_requests_count, 'Merge Request'
+ &nbsp;
+ %span.light #{milestone.percent_complete}% complete
+ .col-sm-6
+ = milestone_progress_bar(milestone)
+ %div
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = milestone.project.name
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 57dc235f5bb..008d5a6bd22 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -18,29 +18,5 @@
.nothing-here-block No milestones to show
- else
- @group_milestones.each do |milestone|
- %li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .pull-right
- - if can?(current_user, :manage_group, @group)
- - if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- - else
- = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
- %h4
- = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
- %div
- %div
- = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
- = pluralize milestone.issue_count, 'Issue'
- &nbsp;
- = link_to group_milestone_path(@group, milestone.safe_title, title: milestone.title) do
- = pluralize milestone.merge_requests_count, 'Merge Request'
- &nbsp;
- %span.light #{milestone.percent_complete}% complete
- = milestone_progress_bar(milestone)
- %div
- %br
- - milestone.milestones.each do |milestone|
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone) do
- %span.label.label-default
- = milestone.project.name
+ = render 'milestone', milestone: milestone
= paginate @group_milestones, theme: "gitlab"
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index fea70f5cbc3..fb32f2caa4c 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -6,7 +6,7 @@
Open
Milestone #{@group_milestone.title}
.pull-right
- - if can?(current_user, :manage_group, @group)
+ - if can?(current_user, :admin_group, @group)
- if @group_milestone.active?
= link_to 'Close Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
- else
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index dd1fa3840d5..0d547984cc9 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -2,7 +2,7 @@
.panel-heading
%strong= @group.name
projects:
- - if can? current_user, :manage_group, @group
+ - if can? current_user, :admin_group, @group
.panel-head-actions
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
%i.fa.fa-plus
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index eca34dbff06..cc1be6a717a 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,2 +1,2 @@
.documentation.wiki
- = markdown File.read(Rails.root.join('doc', @category, @file + '.md')).gsub("$your_email", current_user.email)
+ = markdown @markdown.gsub('$your_email', current_user.email)
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 8d10722628f..90a6f5f9d2d 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -13,7 +13,7 @@
- elsif @access_denied
:plain
job = $("tr#repo_#{@repo_id}")
- job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>"")
+ job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
- else
:plain
job = $("tr#repo_#{@repo_id}")
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
new file mode 100644
index 00000000000..ce78fec205f
--- /dev/null
+++ b/app/views/import/google_code/new.html.haml
@@ -0,0 +1,60 @@
+%h3.page-title
+ %i.fa.fa-google
+ Import projects from Google Code
+%hr
+
+= form_tag callback_import_google_code_path, class: 'form-horizontal', multipart: true do
+ %p
+ Follow the steps below to export your Google Code project data.
+ In the next step, you'll be able to select the projects you want to import.
+ %ol
+ %li
+ %p
+ Go to
+ #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}.
+ %li
+ %p
+ Make sure you're logged into the account that owns the projects you'd like to import.
+ %li
+ %p
+ Click the <strong>Select none</strong> button on the right, since we only need "Google Code Project Hosting".
+ %li
+ %p
+ Scroll down to <strong>Google Code Project Hosting</strong> and enable the switch on the right.
+ %li
+ %p
+ Choose <strong>Next</strong> at the bottom of the page.
+ %li
+ %p
+ Leave the "File type" and "Delivery method" options on their default values.
+ %li
+ %p
+ Choose <strong>Create archive</strong> and wait for archiving to complete.
+ %li
+ %p
+ Click the <strong>Download</strong> button and wait for downloading to complete.
+ %li
+ %p
+ Find the downloaded ZIP file and decompress it.
+ %li
+ %p
+ Find the newly extracted <code>Takeout/Google Code Project Hosting/GoogleCodeProjectHosting.json</code> file.
+ %li
+ %p
+ Upload <code>GoogleCodeProjectHosting.json</code> here:
+ %p
+ %input{type: "file", name: "dump_file", id: "dump_file"}
+ %li
+ %p
+ Do you want to customize how Google Code email addresses and usernames are imported into GitLab?
+ %p
+ = label_tag :create_user_map_0 do
+ = radio_button_tag :create_user_map, 0, true
+ No, directly import the existing email addresses and usernames.
+ %p
+ = label_tag :create_user_map_1 do
+ = radio_button_tag :create_user_map, 1, false
+ Yes, let me map Google Code users to full names or GitLab users.
+ %li
+ %p
+ = submit_tag 'Continue to the next step', class: "btn btn-create"
diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml
new file mode 100644
index 00000000000..d55fcfc97a8
--- /dev/null
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -0,0 +1,20 @@
+%h3.page-title
+ %i.fa.fa-google
+ Import projects from Google Code
+%hr
+
+= form_tag create_user_map_import_google_code_path, class: 'form-horizontal' do
+ %p
+ Customize how Google Code email addresses and usernames are imported into GitLab.
+ In the next step, you'll be able to select the projects you want to import.
+ %p
+ The user map is a JSON document mapping Google Code users (as keys) to the way they will be imported into GitLab (as values). By default the username is masked to ensure users' privacy.
+ %p
+ To map a Google Code user to a full name or GitLab user, simply replace the value, e.g. <code>"johnsmith@gmail.com": "John Smith"</code> or <code>"johnsmith@gmail.com": "@johnsmith"</code>. Be sure to preserve the surrounding double quotes and other punctuation.
+
+ .form-group
+ .col-sm-12
+ = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15
+
+ .form-actions
+ = submit_tag 'Continue to the next step', class: "btn btn-create"
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
new file mode 100644
index 00000000000..2013b8c03c6
--- /dev/null
+++ b/app/views/import/google_code/status.html.haml
@@ -0,0 +1,49 @@
+%h3.page-title
+ %i.fa.fa-google
+ Import projects from Google Code
+
+%p.light
+ Select projects you want to import.
+%p.light
+ Optionally, you can
+ = link_to "customize", new_user_map_import_google_code_path
+ how Google Code email addresses and usernames are imported into GitLab.
+%hr
+%p
+ = button_tag 'Import all projects', class: "btn btn-success js-import-all"
+
+%table.table.import-jobs
+ %thead
+ %tr
+ %th From Google Code
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
+
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+ %td.import-target
+ = "#{current_user.username}/#{repo.name}"
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
+
+:coffeescript
+ new ImporterStatus("#{jobs_import_google_code_path}", "#{import_google_code_path}")
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
new file mode 100644
index 00000000000..ab0ecffe4d2
--- /dev/null
+++ b/app/views/invites/show.html.haml
@@ -0,0 +1,29 @@
+%h3.page-title Invitation
+
+%p
+ You have been invited
+ - if inviter = @member.created_by
+ by
+ = link_to inviter.name, user_url(inviter)
+ to join
+ - case @member.source
+ - when Project
+ - project = @member.source
+ project
+ %strong
+ = link_to project.name_with_namespace, namespace_project_url(project.namespace, project)
+ - when Group
+ - group = @member.source
+ group
+ %strong
+ = link_to group.name, group_url(group)
+ as #{@member.human_access}.
+
+- if @member.source.users.include?(current_user)
+ %p
+ However, you are already a member of this #{@member.source.is_a?(Group) ? "group" : "project"}.
+ Sign in using a different account to accept the invitation.
+- else
+ .actions
+ = link_to "Accept invitation", accept_invite_url(@token), method: :post, class: "btn btn-success"
+ = link_to "Decline", decline_invite_url(@token), method: :post, class: "btn btn-danger prepend-left-10"
diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml
index b1c2e1a7b19..d58582c107a 100644
--- a/app/views/layouts/_head_panel.html.haml
+++ b/app/views/layouts/_head_panel.html.haml
@@ -39,7 +39,7 @@
= link_to profile_path, title: "Profile settings", class: 'has_bottom_tooltip', 'data-original-title' => 'Profile settings"' do
%i.fa.fa-user
%li
- = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Logout", class: 'has_bottom_tooltip', 'data-original-title' => 'Logout' do
+ = link_to destroy_user_session_path, class: "logout", method: :delete, title: "Sign out", class: 'has_bottom_tooltip', 'data-original-title' => 'Sign out' do
%i.fa.fa-sign-out
%li.hidden-xs
= link_to current_user, class: "profile-pic has_bottom_tooltip", id: 'profile-pic', 'data-original-title' => 'Your profile' do
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 2f38d596c65..34efceb37d1 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -19,6 +19,11 @@
%i.fa.fa-group
%span
Groups
+ = nav_link(controller: :deploy_keys) do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
+ %i.fa.fa-key
+ %span
+ Deploy Keys
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs' do
%i.fa.fa-file-text
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 32fe0e37df8..f0d92b7a12c 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -30,7 +30,7 @@
%span
Members
- - if can?(current_user, :manage_group, @group)
+ - if can?(current_user, :admin_group, @group)
= nav_link(html_options: { class: "#{"active" if group_settings_page?} separate-item" }) do
= link_to edit_group_path(@group), title: 'Settings', class: "tab no-highlight" do
%i.fa.fa-cogs
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 52681865d64..6c13f30f627 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -53,7 +53,7 @@
- if project_nav_tab? :issues
= nav_link(controller: :issues) do
- = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do
+ = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues' do
%i.fa.fa-exclamation-circle
%span
Issues
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 7eec93abdf6..00c7cedce40 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -27,8 +27,7 @@
}
.file-stats .deleted-file {
color: #B00;
- }
- #{add_email_highlight_css}
+ }}
%body
%div.content
= yield
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 778a78acf56..3fd4b04ac84 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -1,2 +1,2 @@
%div
- = replace_image_links_with_base64(markdown(@note.note), @note.project)
+ = markdown(@note.note, reference_only_path: false)
diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml
new file mode 100644
index 00000000000..55efad384a7
--- /dev/null
+++ b/app/views/notify/group_invite_accepted_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ #{@group_member.invite_email}, now known as
+ #{link_to @group_member.user.name, user_url(@group_member.user)},
+ has accepted your invitation to join group
+ #{link_to @group.name, group_url(@group)}.
+
diff --git a/app/views/notify/group_invite_accepted_email.text.erb b/app/views/notify/group_invite_accepted_email.text.erb
new file mode 100644
index 00000000000..f8b70f7a5a6
--- /dev/null
+++ b/app/views/notify/group_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= @group_member.invite_email %>, now known as <%= @group_member.user.name %>, has accepted your invitation to join group <%= @group.name %>.
+
+<%= group_url(@group) %>
diff --git a/app/views/notify/group_invite_declined_email.html.haml b/app/views/notify/group_invite_declined_email.html.haml
new file mode 100644
index 00000000000..f9525d84fac
--- /dev/null
+++ b/app/views/notify/group_invite_declined_email.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{@invite_email}
+ has declined your invitation to join group
+ #{link_to @group.name, group_url(@group)}.
+
diff --git a/app/views/notify/group_invite_declined_email.text.erb b/app/views/notify/group_invite_declined_email.text.erb
new file mode 100644
index 00000000000..6c19a288d15
--- /dev/null
+++ b/app/views/notify/group_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join group <%= @group.name %>.
+
+<%= group_url(@group) %>
diff --git a/app/views/notify/group_member_invited_email.html.haml b/app/views/notify/group_member_invited_email.html.haml
new file mode 100644
index 00000000000..163e88bfea3
--- /dev/null
+++ b/app/views/notify/group_member_invited_email.html.haml
@@ -0,0 +1,14 @@
+%p
+ You have been invited
+ - if inviter = @group_member.created_by
+ by
+ = link_to inviter.name, user_url(inviter)
+ to join group
+ = link_to @group.name, group_url(@group)
+ as #{@group_member.human_access}.
+
+%p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
+
diff --git a/app/views/notify/group_member_invited_email.text.erb b/app/views/notify/group_member_invited_email.text.erb
new file mode 100644
index 00000000000..28ce4819b14
--- /dev/null
+++ b/app/views/notify/group_member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{@group_member.created_by.name} " if @group_member.created_by %>to join group <%= @group.name %> as <%= @group_member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/new_issue_email.html.haml b/app/views/notify/new_issue_email.html.haml
index 03cbee94608..53a068be52e 100644
--- a/app/views/notify/new_issue_email.html.haml
+++ b/app/views/notify/new_issue_email.html.haml
@@ -1,5 +1,5 @@
-if @issue.description
- = replace_image_links_with_base64(markdown(@issue.description), @issue.project)
+ = markdown(@issue.description, reference_only_path: false)
- if @issue.assignee_id.present?
%p
diff --git a/app/views/notify/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml
index 729a7bb505d..5b7dd117c16 100644
--- a/app/views/notify/new_merge_request_email.html.haml
+++ b/app/views/notify/new_merge_request_email.html.haml
@@ -6,4 +6,4 @@
Assignee: #{@merge_request.author_name} &rarr; #{@merge_request.assignee_name}
-if @merge_request.description
- = replace_image_links_with_base64(markdown(@merge_request.description), @merge_request.project)
+ = markdown(@merge_request.description, reference_only_path: false)
diff --git a/app/views/notify/project_invite_accepted_email.html.haml b/app/views/notify/project_invite_accepted_email.html.haml
new file mode 100644
index 00000000000..7e58d30b10a
--- /dev/null
+++ b/app/views/notify/project_invite_accepted_email.html.haml
@@ -0,0 +1,6 @@
+%p
+ #{@project_member.invite_email}, now known as
+ #{link_to @project_member.user.name, user_url(@project_member.user)},
+ has accepted your invitation to join project
+ #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
+
diff --git a/app/views/notify/project_invite_accepted_email.text.erb b/app/views/notify/project_invite_accepted_email.text.erb
new file mode 100644
index 00000000000..fcbe752114d
--- /dev/null
+++ b/app/views/notify/project_invite_accepted_email.text.erb
@@ -0,0 +1,3 @@
+<%= @project_member.invite_email %>, now known as <%= @project_member.user.name %>, has accepted your invitation to join project <%= @project.name_with_namespace %>.
+
+<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_invite_declined_email.html.haml b/app/views/notify/project_invite_declined_email.html.haml
new file mode 100644
index 00000000000..c2d7e6f6e3a
--- /dev/null
+++ b/app/views/notify/project_invite_declined_email.html.haml
@@ -0,0 +1,5 @@
+%p
+ #{@invite_email}
+ has declined your invitation to join project
+ #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}.
+
diff --git a/app/views/notify/project_invite_declined_email.text.erb b/app/views/notify/project_invite_declined_email.text.erb
new file mode 100644
index 00000000000..484687fa51c
--- /dev/null
+++ b/app/views/notify/project_invite_declined_email.text.erb
@@ -0,0 +1,3 @@
+<%= @invite_email %> has declined your invitation to join project <%= @project.name_with_namespace %>.
+
+<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/notify/project_member_invited_email.html.haml b/app/views/notify/project_member_invited_email.html.haml
new file mode 100644
index 00000000000..79eb89616de
--- /dev/null
+++ b/app/views/notify/project_member_invited_email.html.haml
@@ -0,0 +1,13 @@
+%p
+ You have been invited
+ - if inviter = @project_member.created_by
+ by
+ = link_to inviter.name, user_url(inviter)
+ to join project
+ = link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)
+ as #{@project_member.human_access}.
+
+%p
+ = link_to 'Accept invitation', invite_url(@token)
+ or
+ = link_to 'decline', decline_invite_url(@token)
diff --git a/app/views/notify/project_member_invited_email.text.erb b/app/views/notify/project_member_invited_email.text.erb
new file mode 100644
index 00000000000..e0706272115
--- /dev/null
+++ b/app/views/notify/project_member_invited_email.text.erb
@@ -0,0 +1,4 @@
+You have been invited <%= "by #{@project_member.created_by.name} " if @project_member.created_by %>to join project <%= @project.name_with_namespace %> as <%= @project_member.human_access %>.
+
+Accept invitation: <%= invite_url(@token) %>
+Decline invitation: <%= decline_invite_url(@token) %>
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index bbf7004c906..a374a662333 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -59,8 +59,7 @@
%strong
= diff.new_path
%hr
- %pre
- = color_email_diff(diff.diff)
+ = color_email_diff(diff.diff)
%br
- if @compare.timeout
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 9d8f33cbbaa..09f290429ea 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -20,9 +20,13 @@
%li
%strong= @primary
%span.label.label-success Primary Email
+ - if @primary === @public_email
+ %span.label.label-info Public Email
- @emails.each do |email|
%li
%strong= email.email
+ - if email.email === @public_email
+ %span.label.label-info Public Email
%span.cgray
added #{time_ago_with_tooltip(email.created_at)}
= link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 5a501e43149..6c745e69e40 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -42,6 +42,11 @@
- else
%span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group
+ = f.label :public_email, class: "control-label"
+ .col-sm-10
+ = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), {include_blank: 'Do not show in profile'}, class: "form-control"
+ %span.help-block This email will be displayed on your public profile.
+ .form-group
= f.label :skype, class: "control-label"
.col-sm-10= f.text_field :skype, class: "form-control"
.form-group
diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml
index f4f4c2662cf..3036f11bb2d 100644
--- a/app/views/projects/_dropdown.html.haml
+++ b/app/views/projects/_dropdown.html.haml
@@ -5,29 +5,33 @@
%ul.dropdown-menu
- if @project.issues_enabled && can?(current_user, :write_issue, @project)
%li
- = link_to url_for_new_issue, title: "New Issue" do
+ = link_to url_for_new_issue(@project, only_path: true), title: "New Issue" do
+ %i.fa.fa-fw.fa-exclamation-circle
New issue
- if @project.merge_requests_enabled && can?(current_user, :write_merge_request, @project)
%li
= link_to new_namespace_project_merge_request_path(@project.namespace, @project), title: "New Merge Request" do
+ %i.fa.fa-fw.fa-tasks
New merge request
- if @project.snippets_enabled && can?(current_user, :write_snippet, @project)
%li
= link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do
+ %i.fa.fa-fw.fa-file-text-o
New snippet
- if can?(current_user, :admin_project_member, @project)
%li
= link_to namespace_project_project_members_path(@project.namespace, @project), title: "New project member" do
+ %i.fa.fa-fw.fa-users
New project member
- if can? current_user, :push_code, @project
%li.divider
%li
= link_to new_namespace_project_branch_path(@project.namespace, @project) do
- %i.fa.fa-code-fork
- Git branch
+ %i.fa.fa-fw.fa-code-fork
+ New branch
%li
= link_to new_namespace_project_tag_path(@project.namespace, @project) do
- %i.fa.fa-tag
- Git tag
+ %i.fa.fa-fw.fa-tag
+ New tag
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index a295a0d6cdc..5689bdee1c6 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -2,7 +2,7 @@
.project-home-panel{:class => ("empty-project" if empty_repo)}
.project-identicon-holder
= project_icon(@project, alt: '', class: 'avatar project-avatar')
- .project-home-row
+ .project-home-row.project-home-row-top
.project-home-desc
- if @project.description.present?
= escaped_autolink(@project.description)
@@ -14,31 +14,30 @@
&ndash;
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
= readme.name
- .star-fork-buttons
+ .project-repo-buttons
+ .inline.star.js-toggler-container{class: @show_star ? 'on' : ''}
+ - if current_user
+ = link_to_toggle_star('Star this project.', false)
+ = link_to_toggle_star('Unstar this project.', true)
+ - else
+ = link_to new_user_session_path, class: 'btn star-btn has_tooltip', title: 'You must sign in to star a project' do
+ %span
+ = icon('star')
+ Star
+ %span.count
+ = @project.star_count
- unless @project.empty_repo?
- .fork-buttons
- - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace
+ - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace
+ .inline.fork-buttons.prepend-left-10
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork' do
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-sm btn-default' do
= link_to_toggle_fork
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project" do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-sm btn-default' do
= link_to_toggle_fork
- .star-buttons
- %span.star.js-toggler-container{class: @show_star ? 'on' : ''}
- - if current_user
- = link_to_toggle_star('Star this project.', false, true)
- = link_to_toggle_star('Unstar this project.', true, true)
- - else
- = link_to_toggle_star('You must sign in to star a project.', false, false)
-
.project-home-row.hidden-xs
- if current_user && !empty_repo
.project-home-dropdown
= render "dropdown"
- - unless @project.empty_repo?
- - if can? current_user, :download_code, @project
- .pull-right.prepend-left-10
- = render 'projects/repositories/download_archive', split_button: true
= render "shared/clone_panel"
diff --git a/app/views/projects/_issuable_form.html.haml b/app/views/projects/_issuable_form.html.haml
index 7fd5fe8a6e1..e321a84974e 100644
--- a/app/views/projects/_issuable_form.html.haml
+++ b/app/views/projects/_issuable_form.html.haml
@@ -35,8 +35,8 @@
%i.fa.fa-user
Assign to
.col-sm-10
- = project_users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select a user', class: 'custom-form-control',
+ = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
+ placeholder: 'Select a user', class: 'custom-form-control', null_user: true,
selected: issuable.assignee_id)
&nbsp;
= link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index ba60bd92869..65c3ab10e02 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -22,7 +22,7 @@
%div#tree-content-holder.tree-content-holder
%article.file-holder
.file-title
- %i.fa.fa-file
+ = blob_icon blob.mode, blob.name
%strong
= blob.name
%small
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 0de8c509f2b..4e7415be4aa 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -11,14 +11,14 @@
protected
.pull-right
- if can?(current_user, :download_code, @project)
- = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-sm'
+ = render 'projects/repositories/download_archive', ref: branch.name, btn_class: 'btn-grouped btn-group-xs'
- if branch.name != @repository.root_ref
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-sm', method: :post, title: "Compare" do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do
%i.fa.fa-files-o
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-sm btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row', method: :delete, data: { confirm: 'Removed branch cannot be restored. Are you sure?'}, remote: true do
%i.fa.fa-trash-o
- if commit
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 2579f2cac92..3f645b81397 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -10,7 +10,8 @@
Download as
%span.caret
%ul.dropdown-menu
- %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch)
+ - unless @commit.parents.length > 1
+ %li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch)
%li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff)
= link_to namespace_project_tree_path(@project.namespace, @project, @commit), class: "btn btn-primary btn-grouped" do
%span Browse Code »
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 83e4d24cf5f..a714f5f79e0 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,6 +1,8 @@
%ul.nav.nav-tabs
= nav_link(controller: [:commit, :commits]) do
- = link_to 'Commits', namespace_project_commits_path(@project.namespace, @project, @repository.root_ref)
+ = link_to namespace_project_commits_path(@project.namespace, @project, @repository.root_ref) do
+ Commits
+ %span.badge= number_with_precision(@repository.commit_count, precision: 0, delimiter: ',')
= nav_link(controller: :compare) do
= link_to 'Compare', namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref)
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index a2faa9d5e25..c577dfa8d55 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -5,21 +5,32 @@
%i.fa.fa-plus
Enable
- else
- - if deploy_key.projects.count > 1
+ - if deploy_key.destroyed_when_orphaned? && deploy_key.almost_orphaned?
+ = link_to 'Remove', disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :put, class: "btn btn-remove delete-key btn-sm pull-right"
+ - else
= link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do
%i.fa.fa-power-off
Disable
- - else
- = link_to 'Remove', namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), data: { confirm: 'You are going to remove deploy key. Are you sure?'}, method: :delete, class: "btn btn-remove delete-key btn-sm pull-right"
-
- - key_project = deploy_key.projects.include?(@project) ? @project : deploy_key.projects.first
- = link_to namespace_project_deploy_key_path(key_project.namespace, key_project, deploy_key) do
+ - if project = project_for_deploy_key(deploy_key)
+ = link_to namespace_project_deploy_key_path(project.namespace, project, deploy_key) do
+ %i.fa.fa-key
+ %strong= deploy_key.title
+ - else
%i.fa.fa-key
%strong= deploy_key.title
+
%p.light.prepend-top-10
- - deploy_key.projects.map(&:name_with_namespace).each do |project_name|
- %span.label.label-gray.deploy-project-label= project_name
+ - if deploy_key.public?
+ %span.label.label-info.deploy-project-label
+ Public deploy key
+
+ - deploy_key.projects.each do |project|
+ - if can?(current_user, :read_project, project)
+ %span.label.label-gray.deploy-project-label
+ = link_to namespace_project_path(project.namespace, project) do
+ = project.name_with_namespace
+
%small.pull-right
Created #{time_ago_with_tooltip(deploy_key.created_at)}
diff --git a/app/views/projects/deploy_keys/index.html.haml b/app/views/projects/deploy_keys/index.html.haml
index c02a18146eb..472a13a8524 100644
--- a/app/views/projects/deploy_keys/index.html.haml
+++ b/app/views/projects/deploy_keys/index.html.haml
@@ -22,11 +22,20 @@
.light-well
.nothing-here-block Create a #{link_to 'new deploy key', new_namespace_project_deploy_key_path(@project.namespace, @project)} or add an existing one
.col-md-6.available-keys
- %h5
- %strong Deploy keys
- from projects available to you
- %ul.bordered-list
- = render @available_keys
- - if @available_keys.blank?
- .light-well
- .nothing-here-block Deploy keys from projects you have access to will be displayed here
+ - # If there are available public deploy keys but no available project deploy keys, only public deploy keys are shown.
+ - if @available_project_keys.any? || @available_public_keys.blank?
+ %h5
+ %strong Deploy keys
+ from projects you have access to
+ %ul.bordered-list
+ = render @available_project_keys
+ - if @available_project_keys.blank?
+ .light-well
+ .nothing-here-block Deploy keys from projects you have access to will be displayed here
+
+ - if @available_public_keys.any?
+ %h5
+ %strong Public deploy keys
+ available to any project
+ %ul.bordered-list
+ = render @available_public_keys
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 2b9b6599a7d..b49aee504fe 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -20,4 +20,4 @@
Maybe diff is really big and operation failed with timeout. Try to get diff locally
:coffeescript
- $('.files .diff-header').stick_in_parent(recalc_every: 1, offset_top: $('.navbar').height())
+ $('.files .diff-header').stick_in_parent(offset_top: $('.navbar').height())
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index e691db9c08e..e6dfbfd6511 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -23,7 +23,7 @@
%td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text)
- if @reply_allowed
- - comments = @line_notes.select { |n| n.line_code == line_code }.sort_by(&:created_at)
+ - comments = @line_notes.select { |n| n.line_code == line_code && n.active? }.sort_by(&:created_at)
- unless comments.empty?
= render "projects/notes/diff_notes_with_reply", notes: comments, line: line.text
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index 4a5d09b9503..78b4c1923dd 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -54,7 +54,7 @@
}
ctx = $("#hour-chart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true});
+ new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
data = {
labels : #{@commits_per_week_days.keys.to_json},
@@ -68,7 +68,7 @@
}
ctx = $("#weekday-chart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true});
+ new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
data = {
labels : #{@commits_per_month.keys.to_json},
@@ -82,4 +82,4 @@
}
ctx = $("#month-chart").get(0).getContext("2d");
- new Chart(ctx).Line(data,{"scaleOverlay": true, responsive: true});
+ new Chart(ctx).Line(data, {"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 0d3028d50b4..288b48f4583 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -9,8 +9,8 @@
.votes-holder.pull-right
#votes= render 'votes/votes_block', votable: @issue
.participants
- %span= pluralize(@issue.participants.count, 'participant')
- - @issue.participants.each do |participant|
+ %span= pluralize(@issue.participants(current_user).count, 'participant')
+ - @issue.participants(current_user).each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
.voting_notes#notes= render "projects/notes/notes_with_form"
%aside.col-md-3
diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml
index c3d6dc2e50b..9228074d833 100644
--- a/app/views/projects/issues/_issue_context.html.haml
+++ b/app/views/projects/issues/_issue_context.html.haml
@@ -8,7 +8,7 @@
- else
none
- if can?(current_user, :modify_issue, @issue)
- = project_users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id)
+ = users_select_tag('issue[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @issue.assignee_id, null_user: true, first_user: true)
%div.prepend-top-20.clearfix
.issuable-context-title
@@ -44,5 +44,3 @@
:coffeescript
new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}")
-
-
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 54e3009cca2..d3c7ae24a75 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -15,15 +15,5 @@
= render 'shared/issuable_filter'
- .clearfix
- .issues_bulk_update.hide
- = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
- = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control')
- = project_users_select_tag('update[assignee_id]', placeholder: 'Assignee')
- = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone")
- = hidden_field_tag 'update[issues_ids]', []
- = hidden_field_tag :state_event, params[:state_event]
- = button_tag "Update issues", class: "btn update_selected_issues btn-save"
-
.issues-holder
= render "issues"
diff --git a/app/views/projects/issues/update.js.haml b/app/views/projects/issues/update.js.haml
index 82c0e653759..1d38662bff8 100644
--- a/app/views/projects/issues/update.js.haml
+++ b/app/views/projects/issues/update.js.haml
@@ -13,5 +13,5 @@
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true})
$('.edit-issue.inline-update input[type="submit"]').hide();
-new ProjectUsersSelect();
+new UsersSelect()
new Issue();
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 1d8eef4e8ce..d986ce67c0c 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -39,7 +39,7 @@
%i.fa.fa-user
Assign to
.col-sm-10
- = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id)
+ = users_select_tag('merge_request[assignee_id]', placeholder: 'Select a user', class: 'custom-form-control', selected: @merge_request.assignee_id, project_id: @merge_request.target_project_id)
&nbsp;
= link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
.form-group
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index a74aede4e6b..cec02de84ca 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -36,17 +36,17 @@
- if @commits.present?
%ul.nav.nav-tabs.merge-request-tabs
- %li.notes-tab{data: {action: 'notes'}}
+ %li.notes-tab{data: {action: 'notes', toggle: 'tab'}}
= link_to merge_request_path(@merge_request) do
%i.fa.fa-comments
Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.count
- %li.commits-tab{data: {action: 'commits'}}
+ %li.commits-tab{data: {action: 'commits', toggle: 'tab'}}
= link_to merge_request_path(@merge_request), title: 'Commits' do
%i.fa.fa-history
Commits
%span.badge= @commits.size
- %li.diffs-tab{data: {action: 'diffs'}}
+ %li.diffs-tab{data: {action: 'diffs', toggle: 'tab'}}
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request) do
%i.fa.fa-list-alt
Changes
diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml
index 80e5c223d60..105562fb05e 100644
--- a/app/views/projects/merge_requests/show/_context.html.haml
+++ b/app/views/projects/merge_requests/show/_context.html.haml
@@ -9,7 +9,7 @@
none
.issuable-context-selectbox
- if can?(current_user, :modify_merge_request, @merge_request)
- = project_users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id)
+ = users_select_tag('merge_request[assignee_id]', placeholder: 'Select assignee', class: 'custom-form-control js-select2 js-assignee', selected: @merge_request.assignee_id, null_user: true)
%div.prepend-top-20.clearfix
.issuable-context-title
diff --git a/app/views/projects/merge_requests/show/_participants.html.haml b/app/views/projects/merge_requests/show/_participants.html.haml
index 4f34af1737d..9c93fa55fe6 100644
--- a/app/views/projects/merge_requests/show/_participants.html.haml
+++ b/app/views/projects/merge_requests/show/_participants.html.haml
@@ -1,4 +1,4 @@
.participants
- %span #{@merge_request.participants.count} participants
- - @merge_request.participants.each do |participant|
+ %span #{@merge_request.participants(current_user).count} participants
+ - @merge_request.participants(current_user).each do |participant|
= link_to_member(@project, participant, name: false, size: 24)
diff --git a/app/views/projects/merge_requests/show/_state_widget.html.haml b/app/views/projects/merge_requests/show/_state_widget.html.haml
index a4f2a890969..44bd9347f51 100644
--- a/app/views/projects/merge_requests/show/_state_widget.html.haml
+++ b/app/views/projects/merge_requests/show/_state_widget.html.haml
@@ -29,7 +29,7 @@
%h4
Merge in progress...
%p
- GitLab tries to merge it right now. During this time merge request is locked and can not be closed.
+ Merging is in progress. While merging this request is locked and cannot be closed.
- unless @commits.any?
%h4 Nothing to merge
diff --git a/app/views/projects/merge_requests/update.js.haml b/app/views/projects/merge_requests/update.js.haml
index f5cc98c7fa4..b4df1d20737 100644
--- a/app/views/projects/merge_requests/update.js.haml
+++ b/app/views/projects/merge_requests/update.js.haml
@@ -2,7 +2,7 @@
$('.context').html("#{escape_javascript(render partial: 'projects/merge_requests/show/context', locals: { issue: @issue })}");
$('.context').effect('highlight');
- new ProjectUsersSelect();
+ new UsersSelect()
$('select.select2').select2({width: 'resolve', dropdownAutoWidth: true});
merge_request = new MergeRequest();
diff --git a/app/views/projects/milestones/_issue.html.haml b/app/views/projects/milestones/_issue.html.haml
index 26c83841a22..88fccfe4981 100644
--- a/app/views/projects/milestones/_issue.html.haml
+++ b/app/views/projects/milestones/_issue.html.haml
@@ -1,9 +1,9 @@
%li{ id: dom_id(issue, 'sortable'), class: 'issue-row', 'data-iid' => issue.iid, 'data-url' => issue_path(issue) }
- %span.str-truncated
+ .pull-right.assignee-icon
+ - if issue.assignee
+ = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16", alt: ''
+ %span
= link_to [@project.namespace.becomes(Namespace), @project, issue] do
%span.cgray ##{issue.iid}
= link_to_gfm issue.title, [@project.namespace.becomes(Namespace), @project, issue], title: issue.title
- .pull-right.assignee-icon
- - if issue.assignee
- = image_tag avatar_icon(issue.assignee.email, 16), class: "avatar s16"
diff --git a/app/views/projects/milestones/_merge_request.html.haml b/app/views/projects/milestones/_merge_request.html.haml
index 42fbd0cd2ca..0d7a118569a 100644
--- a/app/views/projects/milestones/_merge_request.html.haml
+++ b/app/views/projects/milestones/_merge_request.html.haml
@@ -5,4 +5,4 @@
= link_to_gfm merge_request.title, [@project.namespace.becomes(Namespace), @project, merge_request], title: merge_request.title
.pull-right.assignee-icon
- if merge_request.assignee
- = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16"
+ = image_tag avatar_icon(merge_request.assignee.email, 16), class: "avatar s16", alt: ''
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index 7039c85bb2c..62360158ff9 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -11,16 +11,14 @@
%span.cred (Expired)
%small
= milestone.expires_at
- - if milestone.is_empty?
- %span.muted Empty
- - else
- %div
- %div
- = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do
- = pluralize milestone.issues.count, 'Issue'
- &nbsp;
- = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do
- = pluralize milestone.merge_requests.count, 'Merge Request'
- &nbsp;
- %span.light #{milestone.percent_complete}% complete
+ .row
+ .col-sm-6
+ = link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do
+ = pluralize milestone.issues.count, 'Issue'
+ &nbsp;
+ = link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_id: milestone.id) do
+ = pluralize milestone.merge_requests.count, 'Merge Request'
+ &nbsp;
+ %span.light #{milestone.percent_complete}% complete
+ .col-sm-6
= milestone_progress_bar(milestone)
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 69909a8554e..a06c85b4251 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -62,6 +62,10 @@
%i.icon-gitorious.icon-gitorious-small
Gitorious.org
+ = link_to new_import_google_code_path, class: 'btn' do
+ %i.fa.fa-google
+ Google Code
+
= link_to "#", class: 'btn js-toggle-button' do
%i.fa.fa-git
%span Any repo by URL
diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml
index 3561ca49f81..b8068835b3a 100644
--- a/app/views/projects/notes/_discussion.html.haml
+++ b/app/views/projects/notes/_discussion.html.haml
@@ -6,9 +6,8 @@
= image_tag avatar_icon(note.author_email), class: "avatar s40"
.timeline-content
- if note.for_merge_request?
- - if note.outdated?
- = render "projects/notes/discussions/outdated", discussion_notes: discussion_notes
- - else
- = render "projects/notes/discussions/active", discussion_notes: discussion_notes
+ - (active_notes, outdated_notes) = discussion_notes.partition(&:active?)
+ = render "projects/notes/discussions/active", discussion_notes: active_notes if active_notes.length > 0
+ = render "projects/notes/discussions/outdated", discussion_notes: outdated_notes if outdated_notes.length > 0
- else
= render "projects/notes/discussions/commit", discussion_notes: discussion_notes
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index be96c302143..2ada6cb6700 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -12,7 +12,7 @@
.comment-hints.clearfix
.pull-left Comments are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"),{ target: '_blank', tabindex: -1 }}
.pull-right Attach files by dragging &amp; dropping or #{link_to "selecting them", '#', class: 'markdown-selector', tabindex: -1 }.
-
+ .error-alert
.note-form-actions
.buttons
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 71bdf5c8f2a..0728f8fa42b 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -5,7 +5,7 @@
%span.fa.fa-circle
- else
= link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: "avatar s40"
+ = image_tag avatar_icon(note.author_email), class: "avatar s40", alt: ''
.timeline-content
.note-header
.note-actions
@@ -23,7 +23,7 @@
Remove
- if note.system
= link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: "avatar s16"
+ = image_tag avatar_icon(note.author_email), class: "avatar s16", alt: ''
= link_to_member(@project, note.author, avatar: false)
%span.author-username
= '@' + note.author.username
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 0f824bdabf8..d708b01a114 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -1,7 +1,10 @@
= form_for @project_member, as: :project_member, url: namespace_project_project_members_path(@project.namespace, @project), html: { class: 'form-horizontal users-project-form' } do |f|
.form-group
= f.label :user_ids, "People", class: 'control-label'
- .col-sm-10= users_select_tag(:user_ids, multiple: true, class: 'input-large')
+ .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.
.form-group
= f.label :access_level, "Project Access", class: 'control-label'
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
index a07d0762334..635e4d70941 100644
--- a/app/views/projects/project_members/_project_member.html.haml
+++ b/app/views/projects/project_members/_project_member.html.haml
@@ -1,16 +1,32 @@
- user = member.user
-- return unless user
+- return unless user || member.invite?
%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
%span.list-item-name
- = image_tag avatar_icon(user.email, 16), class: "avatar s16"
- %strong= user.name
- %span.cgray= user.username
- - if user == current_user
- %span.label.label-success It's you
- - if user.blocked?
- %label.label.label-danger
- %strong Blocked
+ - if member.user
+ = image_tag avatar_icon(user.email, 16), class: "avatar s16", alt: ''
+ %strong
+ = link_to user.name, user_path(user)
+ %span.cgray= user.username
+ - if user == current_user
+ %span.label.label-success It's you
+ - if user.blocked?
+ %label.label.label-danger
+ %strong Blocked
+ - else
+ = image_tag avatar_icon(member.invite_email, 16), class: "avatar s16", alt: ''
+ %strong
+ = member.invite_email
+ %span.cgray
+ invited
+ - if member.created_by
+ by
+ = link_to member.created_by.name, user_path(member.created_by)
+ = time_ago_with_tooltip(member.created_at)
+
+ - if current_user_can_admin_project
+ = link_to resend_invite_namespace_project_project_member_path(@project.namespace, @project, member), method: :post, class: "btn-xs btn", title: 'Resend invite' do
+ Resend invite
- if current_user_can_admin_project
- unless @project.personal? && user == current_user
@@ -25,12 +41,12 @@
= link_to leave_namespace_project_project_members_path(@project.namespace, @project), data: { confirm: "Leave project?"}, method: :delete, class: "btn-xs btn btn-remove", title: 'Leave project' do
%i.fa.fa-minus.fa-inverse
- else
- = link_to namespace_project_project_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
+ = link_to namespace_project_project_member_path(@project.namespace, @project, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
%i.fa.fa-minus.fa-inverse
.edit-member.hide.js-toggle-content
%br
- = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member.user), remote: true do |f|
+ = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
.prepend-top-10
= f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
.prepend-top-10
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 49ce6c0888e..35c15cf3a9e 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -15,5 +15,5 @@
if(current_url == log_url) {
// Load 10 more commit log for each file in tree
// if we still on the same page
- ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '/', offset: (@offset + @limit))}');
+ ajaxGet('#{logs_file_namespace_project_ref_path(@project.namespace, @project, @ref, @path || '', offset: (@offset + @limit))}');
}
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
index 26669fb00a9..b9486a9b492 100644
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ b/app/views/projects/repositories/_download_archive.html.haml
@@ -3,14 +3,14 @@
- split_button = split_button || false
- if split_button == true
%span.btn-group{class: btn_class}
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do
%i.fa.fa-download
%span Download zip
- %a.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
+ %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
%span.caret
%span.sr-only
Select Archive Format
- %ul.dropdown-menu{ role: 'menu' }
+ %ul.col-xs-10.dropdown-menu{ role: 'menu' }
%li
= link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
%i.fa.fa-download
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index cfa6f558dd6..4464c51744a 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -4,15 +4,27 @@
= render "home_panel"
-- readme = @repository.readme
%ul.nav.nav-tabs
%li.active
= link_to '#tab-activity', 'data-toggle' => 'tab' do
Activity
- - if readme
+ - if @repository.readme
%li
= link_to '#tab-readme', 'data-toggle' => 'tab' do
Readme
+ - if @repository.changelog
+ %li
+ = link_to changelog_url(@project) do
+ Changelog
+ - if @repository.contribution_guide
+ %li
+ = link_to contribution_guide_url(@project) do
+ Contribution guide
+ - if @repository.license
+ %li
+ = link_to license_url(@project) do
+ License
+
.project-home-links
- unless @project.empty_repo?
= link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref)
@@ -48,27 +60,21 @@
- unless @project.empty_repo?
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-block' do
+ %i.fa.fa-exchange
Compare code
+ - if can?(current_user, :download_code, @project)
+ = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-block'
+
- if version = @repository.version
- detail_url = changelog_url(@project) || version_url(@project)
= link_to detail_url, class: 'btn btn-block' do
+ %i.fa.fa-file-text-o
Version:
%span.count
= @repository.blob_by_oid(version.id).data
- - elsif @repository.changelog
- = link_to changelog_url(@project), class: 'btn btn-block' do
- View changelog
-
- - if @repository.contribution_guide
- = link_to contribution_guide_url(@project), class: 'btn btn-block' do
- View contribution guide
- - if @repository.license
- = link_to license_url(@project), class: 'btn btn-block' do
- View license
-
- .prepend-top-10
+ .prepend-top-10.append-bottom-10
%p
%span.light Created on
#{@project.created_at.stamp('Aug 22, 2013')}
@@ -79,8 +85,11 @@
- else
#{link_to @project.owner_name, @project.owner}
+
+ .prepend-top-10
- @project.ci_services.each do |ci_service|
- if ci_service.active? && ci_service.respond_to?(:builds_path)
+ %hr
- if ci_service.respond_to?(:status_img_path)
= link_to ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink' do
= image_tag ci_service.status_img_path, alt: "build status"
@@ -88,7 +97,7 @@
%span.light CI provided by
= link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink'
- - if readme
+ - if readme = @repository.readme
.tab-pane#tab-readme
%article.readme-holder#README
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
@@ -97,4 +106,3 @@
= readme.name
.wiki
= render_readme(readme)
-
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index f22308e54b0..28ad272322f 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -9,9 +9,9 @@
= strip_gpg_signature(tag.message)
.pull-right
- if can? current_user, :download_code, @project
- = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-sm'
+ = render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs'
- if can?(current_user, :admin_project, @project)
- = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-sm btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do
+ = link_to namespace_project_tag_path(@project.namespace, @project, tag.name), class: 'btn btn-xs btn-remove remove-row grouped', method: :delete, data: { confirm: 'Removed tag cannot be restored. Are you sure?'}, remote: true do
%i.fa.fa-trash-o
- if commit
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index b253fe896e3..02ecbade219 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -1,6 +1,6 @@
%tr{ class: "tree-item #{tree_hex_class(blob_item)}" }
%td.tree-item-file-name
- = tree_icon(type)
+ = tree_icon(type, blob_item.mode, blob_item.name)
%span.str-truncated
= link_to blob_item.name, namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name))
%td.tree_time_ago.cgray
diff --git a/app/views/projects/tree/_submodule_item.html.haml b/app/views/projects/tree/_submodule_item.html.haml
index 20c70cac699..2b5f671c09e 100644
--- a/app/views/projects/tree/_submodule_item.html.haml
+++ b/app/views/projects/tree/_submodule_item.html.haml
@@ -1,6 +1,6 @@
%tr{ class: "tree-item" }
%td.tree-item-file-name
- %i.fa.fa-archive
+ %i.fa.fa-archive.fa-fw
= submodule_link(submodule_item, @ref)
%td
%td.hidden-xs
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 94342bc9b2b..e87138bf980 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -1,6 +1,6 @@
%tr{ class: "tree-item #{tree_hex_class(tree_item)}" }
%td.tree-item-file-name
- = tree_icon(type)
+ = tree_icon(type, tree_item.mode, tree_item.name)
%span.str-truncated
- path = flatten_tree(tree_item)
= link_to path, namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path))
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index a1121750ca3..8b1e3a6dd5e 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -15,7 +15,7 @@
:"data-html" => "true",
:"data-container" => "body"}
= gitlab_config.protocol.upcase
- = text_field_tag :project_clone, default_url_to_repo(project), class: "one_click_select form-control", readonly: true
+ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
- if project.kind_of?(Project)
.input-group-addon
.visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" }
diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml
index 5412b9ef0f4..83f5a3a8015 100644
--- a/app/views/shared/_issuable_filter.html.haml
+++ b/app/views/shared/_issuable_filter.html.haml
@@ -14,106 +14,45 @@
%i.fa.fa-compass
All
- %div
- - if controller.controller_name == 'issues'
- .check-all-holder
- = check_box_tag "check_all_issues", nil, false,
- class: "check_all_issues left",
- disabled: !can?(current_user, :modify_issue, @project)
- .issues-other-filters
- .dropdown.inline.assignee-filter
- %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"}
- %i.fa.fa-user
- %span.light assignee:
- - if @assignee.present?
- %strong= @assignee.name
- - elsif params[:assignee_id] == "0"
- Unassigned
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to page_filter_path(assignee_id: nil) do
- Any
- = link_to page_filter_path(assignee_id: 0) do
- Unassigned
- - @assignees.sort_by(&:name).each do |user|
- %li
- = link_to page_filter_path(assignee_id: user.id) do
- = image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
- = user.name
+ .issues-details-filters
+ = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_id, :label_name]), method: :get, class: 'filter-form' do
+ - if controller.controller_name == 'issues'
+ .check-all-holder
+ = check_box_tag "check_all_issues", nil, false,
+ class: "check_all_issues left",
+ disabled: !can?(current_user, :modify_issue, @project)
+ .issues-other-filters
+ .filter-item.inline
+ = users_select_tag(:assignee_id, selected: params[:assignee_id],
+ placeholder: 'Assignee', class: 'trigger-submit', any_user: true, null_user: true, first_user: true)
+
+ .filter-item.inline
+ = users_select_tag(:author_id, selected: params[:author_id],
+ placeholder: 'Author', class: 'trigger-submit', any_user: true, first_user: true)
+
+ .filter-item.inline.milestone-filter
+ = select_tag('milestone_id', projects_milestones_options, class: "select2 trigger-submit", prompt: 'Milestone')
- .dropdown.inline.prepend-left-10.author-filter
- %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"}
- %i.fa.fa-user
- %span.light author:
- - if @author.present?
- %strong= @author.name
- - elsif params[:author_id] == "0"
- Unassigned
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to page_filter_path(author_id: nil) do
- Any
- = link_to page_filter_path(author_id: 0) do
- Unassigned
- - @authors.sort_by(&:name).each do |user|
- %li
- = link_to page_filter_path(author_id: user.id) do
- = image_tag avatar_icon(user.email), class: "avatar s16", alt: ''
- = user.name
+ - if @project
+ .filter-item.inline.labels-filter
+ = select_tag('label_name', project_labels_options(@project), class: "select2 trigger-submit", prompt: 'Label')
- .dropdown.inline.prepend-left-10.milestone-filter
- %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"}
- %i.fa.fa-clock-o
- %span.light milestone:
- - if @milestone.present?
- %strong= @milestone.title
- - elsif params[:milestone_id] == "0"
- None (backlog)
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to page_filter_path(milestone_id: nil) do
- Any
- = link_to page_filter_path(milestone_id: 0) do
- None (backlog)
- - @milestones.each do |milestone|
- %li
- = link_to page_filter_path(milestone_id: milestone.id) do
- %strong= milestone.title
- %small.light= milestone.expires_at
+ .pull-right
+ = render 'shared/sort_dropdown'
+
+ - if controller.controller_name == 'issues'
+ .issues_bulk_update.hide
+ = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post do
+ = select_tag('update[state_event]', options_for_select([['Open', 'reopen'], ['Closed', 'close']]), prompt: "Status", class: 'form-control')
+ = users_select_tag('update[assignee_id]', placeholder: 'Assignee', null_user: true)
+ = select_tag('update[milestone_id]', bulk_update_milestone_options, prompt: "Milestone")
+ = hidden_field_tag 'update[issues_ids]', []
+ = hidden_field_tag :state_event, params[:state_event]
+ = button_tag "Update issues", class: "btn update_selected_issues btn-save"
- - if @project
- .dropdown.inline.prepend-left-10.labels-filter
- %button.dropdown-toggle.btn{type: 'button', "data-toggle" => "dropdown"}
- %i.fa.fa-tags
- %span.light label:
- - if params[:label_name].present?
- %strong= params[:label_name]
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to page_filter_path(label_name: nil) do
- Any
- - if @project.labels.any?
- - @project.labels.each do |label|
- %li
- = link_to page_filter_path(label_name: label.name) do
- = render_colored_label(label)
- - else
- %li
- = link_to generate_namespace_project_labels_path(@project.namespace, @project, redirect: request.original_url), method: :post do
- %i.fa.fa-plus-circle
- Create default labels
+:coffeescript
+ new UsersSelect()
- .pull-right
- = render 'shared/sort_dropdown'
+ $('form.filter-form').on 'submit', (event) ->
+ event.preventDefault()
+ Turbolinks.visit @.action + '&' + $(@).serialize()
diff --git a/app/views/shared/_project.html.haml b/app/views/shared/_project.html.haml
index 8746970c239..722a7f7ce0f 100644
--- a/app/views/shared/_project.html.haml
+++ b/app/views/shared/_project.html.haml
@@ -1,4 +1,4 @@
-= cache [project, controller.controller_name, controller.action_name] do
+= cache [project.namespace, project, controller.controller_name, controller.action_name] do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 55a990c94ed..5204fb9a907 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -17,7 +17,7 @@
%span.light
by
= link_to user_snippets_path(@snippet.author) do
- = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16"
+ = image_tag avatar_icon(@snippet.author_email), class: "avatar avatar-inline s16", alt: ''
= @snippet.author_name
.back-link
diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml
index bca71444956..90d9980c85c 100644
--- a/app/views/users/_profile.html.haml
+++ b/app/views/users/_profile.html.haml
@@ -5,6 +5,10 @@
%li
%span.light Member since
%strong= user.created_at.stamp("Aug 21, 2011")
+ - unless user.public_email.blank?
+ %li
+ %span.light E-mail:
+ %strong= link_to user.public_email, "mailto:#{user.public_email}"
- unless user.skype.blank?
%li
%span.light Skype:
diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml
index b7383d5594e..297fa537394 100644
--- a/app/views/users/_projects.html.haml
+++ b/app/views/users/_projects.html.haml
@@ -1,13 +1,13 @@
-- if @contributed_projects.present?
+- if local_assigns.has_key?(:contributed_projects) && contributed_projects.present?
.panel.panel-default.contributed-projects
.panel-heading Projects contributed to
= render 'shared/projects_list',
- projects: @contributed_projects.sort_by(&:star_count).reverse,
+ projects: contributed_projects.sort_by(&:star_count).reverse,
projects_limit: 5, stars: true, avatar: false
-- if @projects.present?
+- if local_assigns.has_key?(:projects) && projects.present?
.panel.panel-default
.panel-heading Personal projects
= render 'shared/projects_list',
- projects: @projects.sort_by(&:star_count).reverse,
+ projects: projects.sort_by(&:star_count).reverse,
projects_limit: 10, stars: true, avatar: false
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 5e1d65e2ed8..9dd8cb0738c 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -44,7 +44,7 @@
= spinner
%aside.col-md-4
= render 'profile', user: @user
- = render 'projects'
+ = render 'projects', projects: @projects, contributed_projects: @contributed_projects
:coffeescript
$(".user-calendar").load("#{user_calendar_path}")
diff --git a/app/workers/fork_registration_worker.rb b/app/workers/fork_registration_worker.rb
new file mode 100644
index 00000000000..fffa8b3a659
--- /dev/null
+++ b/app/workers/fork_registration_worker.rb
@@ -0,0 +1,12 @@
+class ForkRegistrationWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(from_project_id, to_project_id, private_token)
+ from_project = Project.find(from_project_id)
+ to_project = Project.find(to_project_id)
+
+ from_project.gitlab_ci_service.fork_registration(to_project, private_token)
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 0c3ee6ba4ff..33d8cc8861b 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -11,8 +11,8 @@ class PostReceive
log("Check gitlab.yml config for correct gitlab_shell.repos_path variable. \"#{Gitlab.config.gitlab_shell.repos_path}\" does not match \"#{repo_path}\"")
end
- repo_path.gsub!(/\.git$/, "")
- repo_path.gsub!(/^\//, "")
+ repo_path.gsub!(/\.git\z/, "")
+ repo_path.gsub!(/\A\//, "")
project = Project.find_with_namespace(repo_path)
diff --git a/app/workers/repository_archive_worker.rb b/app/workers/repository_archive_worker.rb
new file mode 100644
index 00000000000..021c1139568
--- /dev/null
+++ b/app/workers/repository_archive_worker.rb
@@ -0,0 +1,43 @@
+class RepositoryArchiveWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :archive_repo
+
+ attr_accessor :project, :ref, :format
+
+ def perform(project_id, ref, format)
+ @project = Project.find(project_id)
+ @ref, @format = ref, format.downcase
+
+ repository = project.repository
+
+ repository.clean_old_archives
+
+ return unless file_path
+ return if archived? || archiving?
+
+ repository.archive_repo(ref, storage_path, format)
+ end
+
+ private
+
+ def storage_path
+ Gitlab.config.gitlab.repository_downloads_path
+ end
+
+ def file_path
+ @file_path ||= project.repository.archive_file_path(ref, storage_path, format)
+ end
+
+ def pid_file_path
+ @pid_file_path ||= project.repository.archive_pid_file_path(ref, storage_path, format)
+ end
+
+ def archived?
+ File.exist?(file_path)
+ end
+
+ def archiving?
+ File.exist?(pid_file_path)
+ end
+end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 437640d2305..e6a50afedb1 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -18,6 +18,8 @@ class RepositoryImportWorker
Gitlab::GitlabImport::Importer.new(project).execute
elsif project.import_type == 'bitbucket'
Gitlab::BitbucketImport::Importer.new(project).execute
+ elsif project.import_type == 'google_code'
+ Gitlab::GoogleCodeImport::Importer.new(project).execute
else
true
end
diff --git a/bin/background_jobs b/bin/background_jobs
index 59a51c5c868..a041a4b0433 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -37,7 +37,7 @@ start_no_deamonize()
start_sidekiq()
{
- bundle exec sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
+ bundle exec sidekiq -q post_receive -q mailer -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e $RAILS_ENV -P $sidekiq_pidfile $@ >> $sidekiq_logfile 2>&1
}
load_ok()
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 11278ef40dc..ba40671b162 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -49,6 +49,7 @@ production: &base
# Email address used in the "From" field in mails sent by GitLab
email_from: example@example.com
email_display_name: GitLab
+ email_reply_to: noreply@example.com
# Email server smtp settings are in config/initializers/smtp_settings.rb.sample
@@ -66,8 +67,8 @@ production: &base
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
# This happens when the commit is pushed or merged into the default branch of a project.
# When not specified the default issue_closing_pattern as specified below will be used.
- # Tip: you can test your closing pattern at http://rubular.com
- # issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)'
+ # Tip: you can test your closing pattern at http://rubular.com.
+ # issue_closing_pattern: '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)'
## Default project features settings
default_projects_features:
@@ -86,35 +87,6 @@ production: &base
# The default is 'tmp/repositories' relative to the root of the Rails app.
# repository_downloads_path: tmp/repositories
- ## External issues trackers
- issues_tracker:
- # redmine:
- # title: "Redmine"
- # ## If not nil, link 'Issues' on project page will be replaced with this
- # ## Use placeholders:
- # ## :project_id - GitLab project identifier
- # ## :issues_tracker_id - Project Name or Id in external issue tracker
- # project_url: "http://redmine.sample/projects/:issues_tracker_id"
- #
- # ## If not nil, links from /#\d/ entities from commit messages will replaced with this
- # ## Use placeholders:
- # ## :project_id - GitLab project identifier
- # ## :issues_tracker_id - Project Name or Id in external issue tracker
- # ## :id - Issue id (from commit messages)
- # issues_url: "http://redmine.sample/issues/:id"
- #
- # ## If not nil, links to creating new issues will be replaced with this
- # ## Use placeholders:
- # ## :project_id - GitLab project identifier
- # ## :issues_tracker_id - Project Name or Id in external issue tracker
- # new_issue_url: "http://redmine.sample/projects/:issues_tracker_id/issues/new"
- #
- # jira:
- # title: "Atlassian Jira"
- # project_url: "http://jira.sample/issues/?jql=project=:issues_tracker_id"
- # issues_url: "http://jira.sample/browse/:id"
- # new_issue_url: "http://jira.sample/secure/CreateIssue.jspa"
-
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
@@ -133,6 +105,15 @@ production: &base
ldap:
enabled: false
servers:
+ ##########################################################################
+ #
+ # Since GitLab 7.4, LDAP servers get ID's (below the ID is 'main'). GitLab
+ # Enterprise Edition now supports connecting to multiple LDAP servers.
+ #
+ # If you are updating from the old (pre-7.4) syntax, you MUST give your
+ # old server the ID 'main'.
+ #
+ ##########################################################################
main: # 'main' is the GitLab 'provider ID' of this LDAP server
## label
#
@@ -165,6 +146,11 @@ production: &base
# disable this setting, because the userPrincipalName contains an '@'.
allow_username_or_email_login: false
+ # To maintain tight control over the number of active users on your GitLab installation,
+ # enable this setting to keep new users blocked until they have been cleared by the admin
+ # (default: false).
+ block_auto_created_users: false
+
# Base where we can search for users
#
# Ex. ou=People,dc=gitlab,dc=example
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 15c1ae9466f..0abd34fc3e0 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -1,3 +1,5 @@
+require 'gitlab' # Load lib/gitlab.rb as soon as possible
+
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
namespace Rails.env
@@ -64,15 +66,17 @@ Settings.ldap['enabled'] = false if Settings.ldap['enabled'].nil?
# backwards compatibility, we only have one host
if Settings.ldap['enabled'] || Rails.env.test?
if Settings.ldap['host'].present?
+ # We detected old LDAP configuration syntax. Update the config to make it
+ # look like it was entered with the new syntax.
server = Settings.ldap.except('sync_time')
- server['provider_name'] = 'ldap'
Settings.ldap['servers'] = {
- 'ldap' => server
+ 'main' => server
}
end
Settings.ldap['servers'].each do |key, server|
server['label'] ||= 'LDAP'
+ 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?
server['provider_name'] ||= "ldap#{key}".downcase
@@ -80,6 +84,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
end
end
+
Settings['omniauth'] ||= Settingslogic.new({})
Settings.omniauth['enabled'] = false if Settings.omniauth['enabled'].nil?
Settings.omniauth['providers'] ||= []
@@ -103,6 +108,7 @@ 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['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
Settings.gitlab['user_home'] ||= begin
@@ -119,6 +125,7 @@ Settings.gitlab['username_changing_enabled'] = true if Settings.gitlab['username
Settings.gitlab['issue_closing_pattern'] = '((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?|[Rr]esolv(?:e[sd]?|ing)) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)' if Settings.gitlab['issue_closing_pattern'].nil?
Settings.gitlab['default_projects_features'] ||= {}
Settings.gitlab['webhook_timeout'] ||= 10
+Settings.gitlab['max_attachment_size'] ||= 10
Settings.gitlab.default_projects_features['issues'] = true if Settings.gitlab.default_projects_features['issues'].nil?
Settings.gitlab.default_projects_features['merge_requests'] = true if Settings.gitlab.default_projects_features['merge_requests'].nil?
Settings.gitlab.default_projects_features['wiki'] = true if Settings.gitlab.default_projects_features['wiki'].nil?
diff --git a/config/initializers/2_app.rb b/config/initializers/2_app.rb
index 655590dff0b..688cdf5f4b0 100644
--- a/config/initializers/2_app.rb
+++ b/config/initializers/2_app.rb
@@ -6,8 +6,3 @@ module Gitlab
Settings
end
end
-
-#
-# Load all libs for threadsafety
-#
-Dir["#{Rails.root}/lib/**/*.rb"].each { |file| require file }
diff --git a/config/initializers/5_backend.rb b/config/initializers/5_backend.rb
index 7c2e7f39000..80d641d73a3 100644
--- a/config/initializers/5_backend.rb
+++ b/config/initializers/5_backend.rb
@@ -6,3 +6,10 @@ require Rails.root.join("lib", "gitlab", "backend", "shell")
# GitLab shell adapter
require Rails.root.join("lib", "gitlab", "backend", "shell_adapter")
+
+required_version = Gitlab::VersionInfo.parse(Gitlab::Shell.version_required)
+current_version = Gitlab::VersionInfo.parse(Gitlab::Shell.new.version)
+
+unless current_version.valid? && required_version <= current_version
+ warn "WARNING: This version of GitLab depends on gitlab-shell #{required_version}, but you're running #{current_version}. Please update gitlab-shell."
+end
diff --git a/config/initializers/acts_as_taggable_on_patch.rb b/config/initializers/acts_as_taggable_on_patch.rb
deleted file mode 100644
index 0d535cb5cac..00000000000
--- a/config/initializers/acts_as_taggable_on_patch.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-# This is a patch to address the issue in https://github.com/mbleigh/acts-as-taggable-on/issues/427 caused by
-# https://github.com/rails/rails/commit/31a43ebc107fbd50e7e62567e5208a05909ec76c
-# gem 'acts-as-taggable-on' has the fix included https://github.com/mbleigh/acts-as-taggable-on/commit/89bbed3864a9252276fb8dd7d535fce280454b90
-# but not in the currently used version of gem ('2.4.1')
-# With replacement of 'acts-as-taggable-on' gem this file will become obsolete
-
-module ActsAsTaggableOn::Taggable
- module Core
- module ClassMethods
- def tagged_with(tags, options = {})
- tag_list = ActsAsTaggableOn::TagList.from(tags)
- empty_result = where("1 = 0")
-
- return empty_result if tag_list.empty?
-
- joins = []
- conditions = []
- having = []
- select_clause = []
-
- context = options.delete(:on)
- owned_by = options.delete(:owned_by)
- alias_base_name = undecorated_table_name.gsub('.','_')
- quote = ActsAsTaggableOn::Tag.using_postgresql? ? '"' : ''
-
- if options.delete(:exclude)
- if options.delete(:wild)
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
- else
- tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
- end
-
- conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)})"
-
- if owned_by
- joins << "JOIN #{ActsAsTaggableOn::Tagging.table_name}" +
- " ON #{ActsAsTaggableOn::Tagging.table_name}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
- " AND #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name, nil)}" +
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = #{owned_by.id}" +
- " AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = #{quote_value(owned_by.class.base_class.to_s, nil)}"
- end
-
- elsif options.delete(:any)
- # get tags, drop out if nothing returned (we need at least one)
- tags =
- if options.delete(:wild)
- ActsAsTaggableOn::Tag.named_like_any(tag_list)
- else
- ActsAsTaggableOn::Tag.named_any(tag_list)
- end
-
- return empty_result unless tags.length > 0
-
- # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
- # avoid ambiguous column name
- taggings_context = context ? "_#{context}" : ''
-
- taggings_alias = adjust_taggings_alias(
- "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
- )
-
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
-
- # don't need to sanitize sql, map all ids and join with OR logic
- conditions << tags.map { |t| "#{taggings_alias}.tag_id = #{t.id}" }.join(" OR ")
- select_clause = "DISTINCT #{table_name}.*" unless context and tag_types.one?
-
- if owned_by
- tagging_join << " AND " +
- sanitize_sql([
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
- owned_by.id,
- owned_by.class.base_class.to_s
- ])
- end
-
- joins << tagging_join
- else
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
-
- return empty_result unless tags.length == tag_list.length
-
- tags.each do |tag|
- taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
- tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}" +
- " AND #{taggings_alias}.tag_id = #{tag.id}"
-
- tagging_join << " AND " + sanitize_sql(["#{taggings_alias}.context = ?", context.to_s]) if context
-
- if owned_by
- tagging_join << " AND " +
- sanitize_sql([
- "#{taggings_alias}.tagger_id = ? AND #{taggings_alias}.tagger_type = ?",
- owned_by.id,
- owned_by.class.base_class.to_s
- ])
- end
-
- joins << tagging_join
- end
- end
-
- taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
-
- if options.delete(:match_all)
- joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
- " ON #{taggings_alias}.taggable_id = #{quote}#{table_name}#{quote}.#{primary_key}" +
- " AND #{taggings_alias}.taggable_type = #{quote_value(base_class.name, nil)}"
-
-
- group_columns = ActsAsTaggableOn::Tag.using_postgresql? ? grouped_column_names_for(self) : "#{table_name}.#{primary_key}"
- group = group_columns
- having = "COUNT(#{taggings_alias}.taggable_id) = #{tags.size}"
- end
-
- select(select_clause) \
- .joins(joins.join(" ")) \
- .where(conditions.join(" AND ")) \
- .group(group) \
- .having(having) \
- .order(options[:order]) \
- .readonly(false)
- end
- end
- end
-end
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index 79abe3c695d..9dce495106f 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -208,7 +208,7 @@ Devise.setup do |config|
if Gitlab::LDAP::Config.enabled?
Gitlab.config.ldap.servers.values.each do |server|
if server['allow_username_or_email_login']
- email_stripping_proc = ->(name) {name.gsub(/@.*$/,'')}
+ email_stripping_proc = ->(name) {name.gsub(/@.*\z/,'')}
else
email_stripping_proc = ->(name) {name}
end
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index 9da7ebf4290..d422acb31d6 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -11,7 +11,7 @@ Doorkeeper.configure do
end
resource_owner_from_credentials do |routes|
- u = User.find_by(email: params[:username])
+ u = User.find_by(email: params[:username]) || User.find_by(username: params[:username])
u if u && u.valid_password?(params[:password])
end
@@ -83,7 +83,7 @@ Doorkeeper.configure do
#
# If not specified, Doorkeeper enables all the four grant flows.
#
- # grant_flows %w(authorization_code implicit password client_credentials)
+ grant_flows %w(authorization_code password client_credentials)
# Under some circumstances you might want to have applications auto-approved,
# so that the user skips the authorization step.
diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb
index 8f8bef42bef..ca58ae92d1b 100644
--- a/config/initializers/mime_types.rb
+++ b/config/initializers/mime_types.rb
@@ -6,3 +6,5 @@
Mime::Type.register_alias "text/plain", :diff
Mime::Type.register_alias "text/plain", :patch
+Mime::Type.register_alias 'text/html', :markdown
+Mime::Type.register_alias 'text/html', :md
diff --git a/config/routes.rb b/config/routes.rb
index c30cd768572..744a99feded 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -8,6 +8,11 @@ Gitlab::Application.routes.draw do
authorizations: 'oauth/authorizations'
end
+ # Autocomplete
+ get '/autocomplete/users' => 'autocomplete#users'
+ get '/autocomplete/users/:id' => 'autocomplete#user'
+
+
# Search
get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
@@ -34,7 +39,7 @@ Gitlab::Application.routes.draw do
# Help
get 'help' => 'help#index'
- get 'help/:category/:file' => 'help#show', as: :help_page
+ get 'help/:category/:file' => 'help#show', as: :help_page, constraints: { category: /.*/, file: /[^\/\.]+/ }
get 'help/shortcuts'
get 'help/ui' => 'help#ui'
@@ -48,6 +53,16 @@ Gitlab::Application.routes.draw do
end
get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ }
+ #
+ # Invites
+ #
+
+ resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
+ member do
+ post :accept
+ match :decline, via: [:get, :post]
+ end
+ end
#
# Import
@@ -76,6 +91,15 @@ Gitlab::Application.routes.draw do
get :callback
get :jobs
end
+
+ resource :google_code, only: [:create, :new], controller: :google_code do
+ get :status
+ post :callback
+ get :jobs
+
+ get :new_user_map, path: :user_map
+ post :create_user_map, path: :user_map
+ end
end
#
@@ -86,18 +110,18 @@ Gitlab::Application.routes.draw do
# Note attachments and User/Group/Project avatars
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
- constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /.+/ }
+ constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
- constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /.+/ }
+ constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
end
# Redirect old note attachments path to new uploads path.
get "files/note/:id/:filename",
to: redirect("uploads/note/attachment/%{id}/%{filename}"),
- constraints: { filename: /.+/ }
+ constraints: { filename: /[^\/]+/ }
#
# Explore area
@@ -140,6 +164,8 @@ Gitlab::Application.routes.draw do
end
end
+ resources :deploy_keys, only: [:index, :show, :new, :create, :destroy]
+
resources :hooks, only: [:index, :create, :destroy] do
get :test
end
@@ -244,6 +270,7 @@ Gitlab::Application.routes.draw do
scope module: :groups do
resources :group_members, only: [:index, :create, :update, :destroy] do
+ post :resend_invite, on: :member
delete :leave, on: :collection
end
@@ -388,7 +415,7 @@ Gitlab::Application.routes.draw do
end
end
- resources :deploy_keys, constraints: { id: /\d+/ } do
+ resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :show, :new, :create] do
member do
put :enable
put :disable
@@ -470,6 +497,10 @@ Gitlab::Application.routes.draw do
get :import
post :apply_import
end
+
+ member do
+ post :resend_invite
+ end
end
resources :notes, only: [:index, :create, :destroy, :update], constraints: { id: /\d+/ } do
@@ -480,7 +511,7 @@ Gitlab::Application.routes.draw do
resources :uploads, only: [:create] do
collection do
- get ":secret/:filename", action: :show, as: :show, constraints: { filename: /.+/ }
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
end
diff --git a/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb
new file mode 100644
index 00000000000..dc38b0eceb7
--- /dev/null
+++ b/db/migrate/20150324133047_remove_periods_at_ends_of_usernames.rb
@@ -0,0 +1,76 @@
+class RemovePeriodsAtEndsOfUsernames < ActiveRecord::Migration
+ include Gitlab::ShellAdapter
+
+ class Namespace < ActiveRecord::Base
+ class << self
+ def find_by_path_or_name(path)
+ find_by("lower(path) = :path OR lower(name) = :path", path: path.downcase)
+ end
+
+ def clean_path(path)
+ path = path.dup
+ path.gsub!(/@.*\z/, "")
+ path.gsub!(/\.git\z/, "")
+ path.gsub!(/\A-+/, "")
+ path.gsub!(/\.+\z/, "")
+ path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+
+ counter = 0
+ base = path
+ while Namespace.find_by_path_or_name(path)
+ counter += 1
+ path = "#{base}#{counter}"
+ end
+
+ path
+ end
+ end
+ end
+
+ def up
+ changed_paths = {}
+
+ select_all("SELECT id, username FROM users WHERE username LIKE '%.'").each do |user|
+ username_was = user["username"]
+ username = Namespace.clean_path(username_was)
+ changed_paths[username_was] = username
+
+ username = quote_string(username)
+ execute "UPDATE users SET username = '#{username}' WHERE id = #{user["id"]}"
+ execute "UPDATE namespaces SET path = '#{username}', name = '#{username}' WHERE type IS NULL AND owner_id = #{user["id"]}"
+ end
+
+ select_all("SELECT id, path FROM namespaces WHERE type = 'Group' AND path LIKE '%.'").each do |group|
+ path_was = group["path"]
+ path = Namespace.clean_path(path_was)
+ changed_paths[path_was] = path
+
+ path = quote_string(path)
+ execute "UPDATE namespaces SET path = '#{path}' WHERE id = #{group["id"]}"
+ end
+
+ changed_paths.each do |path_was, path|
+ if gitlab_shell.mv_namespace(path_was, path)
+ # If repositories moved successfully we need to remove old satellites
+ # and send update instructions to users.
+ # However we cannot allow rollback since we moved namespace dir
+ # So we basically we mute exceptions in next actions
+ begin
+ gitlab_shell.rm_satellites(path_was)
+ # We cannot send update instructions since models and mailers
+ # can't safely be used from migrations as they may be written for
+ # later versions of the database.
+ # send_update_instructions
+ rescue
+ # Returning false does not rollback after_* transaction but gives
+ # us information about failing some of tasks
+ false
+ end
+ else
+ # if we cannot move namespace directory we should rollback
+ # db changes in order to prevent out of sync between db and fs
+ raise Exception.new('namespace directory cannot be moved')
+ end
+ end
+ end
+end
diff --git a/db/migrate/20150327122227_add_public_to_key.rb b/db/migrate/20150327122227_add_public_to_key.rb
new file mode 100644
index 00000000000..6ffbf4cda19
--- /dev/null
+++ b/db/migrate/20150327122227_add_public_to_key.rb
@@ -0,0 +1,5 @@
+class AddPublicToKey < ActiveRecord::Migration
+ def change
+ add_column :keys, :public, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20150327150017_add_import_data_to_project.rb b/db/migrate/20150327150017_add_import_data_to_project.rb
new file mode 100644
index 00000000000..12c00339eec
--- /dev/null
+++ b/db/migrate/20150327150017_add_import_data_to_project.rb
@@ -0,0 +1,5 @@
+class AddImportDataToProject < ActiveRecord::Migration
+ def change
+ add_column :projects, :import_data, :text
+ end
+end
diff --git a/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb
new file mode 100644
index 00000000000..1d161674a9a
--- /dev/null
+++ b/db/migrate/20150328132231_add_max_attachment_size_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddMaxAttachmentSizeToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :max_attachment_size, :integer, default: 10, null: false
+ end
+end
diff --git a/db/migrate/20150406133311_add_invite_data_to_member.rb b/db/migrate/20150406133311_add_invite_data_to_member.rb
new file mode 100644
index 00000000000..3452fd45c4f
--- /dev/null
+++ b/db/migrate/20150406133311_add_invite_data_to_member.rb
@@ -0,0 +1,12 @@
+class AddInviteDataToMember < ActiveRecord::Migration
+ def change
+ add_column :members, :created_by_id, :integer
+ add_column :members, :invite_email, :string
+ add_column :members, :invite_token, :string
+ add_column :members, :invite_accepted_at, :datetime
+
+ change_column :members, :user_id, :integer, null: true
+
+ add_index :members, :invite_token, unique: true
+ end
+end
diff --git a/db/migrate/20150411000035_fix_identities.rb b/db/migrate/20150411000035_fix_identities.rb
new file mode 100644
index 00000000000..d9051f9fffd
--- /dev/null
+++ b/db/migrate/20150411000035_fix_identities.rb
@@ -0,0 +1,45 @@
+class FixIdentities < ActiveRecord::Migration
+ def up
+ # Up until now, legacy 'ldap' references in the database were charitably
+ # interpreted to point to the first LDAP server specified in the GitLab
+ # configuration. So if the database said 'provider: ldap' but the first
+ # LDAP server was called 'ldapmain', then we would try to interpret
+ # 'provider: ldap' as if it said 'provider: ldapmain'. This migration (and
+ # accompanying changes in the GitLab LDAP code) get rid of this complicated
+ # behavior. Any database references to 'provider: ldap' get rewritten to
+ # whatever the code would have interpreted it as, i.e. as a reference to
+ # the first LDAP server specified in gitlab.yml / gitlab.rb.
+ new_provider = if Gitlab.config.ldap.enabled
+ first_ldap_server = Gitlab.config.ldap.servers.values.first
+ first_ldap_server['provider_name']
+ else
+ 'ldapmain'
+ end
+
+ # Delete duplicate identities
+ # We use a sort of self-join to find rows in identities which match on
+ # user_id but where one has provider 'ldap'. We delete the duplicate row
+ # with provider 'ldap'.
+ delete_statement = ''
+ case adapter_name.downcase
+ when /^mysql/
+ delete_statement << 'DELETE FROM id1 USING identities AS id1, identities AS id2'
+ when 'postgresql'
+ delete_statement << 'DELETE FROM identities AS id1 USING identities AS id2'
+ else
+ raise "Unknown DB adapter: #{adapter_name}"
+ end
+ delete_statement << " WHERE id1.user_id = id2.user_id AND id1.provider = 'ldap' AND id2.provider = '#{new_provider}'"
+ execute delete_statement
+
+ # Update legacy identities
+ execute "UPDATE identities SET provider = '#{new_provider}' WHERE provider = 'ldap'"
+
+ if table_exists?('ldap_group_links')
+ execute "UPDATE ldap_group_links SET provider = '#{new_provider}' WHERE provider IS NULL OR provider = 'ldap'"
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20150411180045_rename_buildbox_service.rb b/db/migrate/20150411180045_rename_buildbox_service.rb
new file mode 100644
index 00000000000..5a0b5d07e50
--- /dev/null
+++ b/db/migrate/20150411180045_rename_buildbox_service.rb
@@ -0,0 +1,9 @@
+class RenameBuildboxService < ActiveRecord::Migration
+ def up
+ execute "UPDATE services SET type = 'BuildkiteService' WHERE type = 'BuildboxService';"
+ end
+
+ def down
+ execute "UPDATE services SET type = 'BuildboxService' WHERE type = 'BuildkiteService';"
+ end
+end
diff --git a/db/migrate/20150413192223_add_public_email_to_users.rb b/db/migrate/20150413192223_add_public_email_to_users.rb
new file mode 100644
index 00000000000..700e9f343a6
--- /dev/null
+++ b/db/migrate/20150413192223_add_public_email_to_users.rb
@@ -0,0 +1,5 @@
+class AddPublicEmailToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :public_email, :string, default: "", null: false
+ end
+end
diff --git a/db/migrate/20150417121913_create_project_import_data.rb b/db/migrate/20150417121913_create_project_import_data.rb
new file mode 100644
index 00000000000..c78f5fde85e
--- /dev/null
+++ b/db/migrate/20150417121913_create_project_import_data.rb
@@ -0,0 +1,8 @@
+class CreateProjectImportData < ActiveRecord::Migration
+ def change
+ create_table :project_import_data do |t|
+ t.references :project
+ t.text :data
+ end
+ end
+end
diff --git a/db/migrate/20150417122318_remove_import_data_from_project.rb b/db/migrate/20150417122318_remove_import_data_from_project.rb
new file mode 100644
index 00000000000..c275b49d228
--- /dev/null
+++ b/db/migrate/20150417122318_remove_import_data_from_project.rb
@@ -0,0 +1,5 @@
+class RemoveImportDataFromProject < ActiveRecord::Migration
+ def change
+ remove_column :projects, :import_data
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 4a445ae5832..1aee37b2e61 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: 20150324155957) do
+ActiveRecord::Schema.define(version: 20150417122318) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -28,6 +28,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do
t.integer "default_branch_protection", default: 2
t.boolean "twitter_sharing_enabled", default: true
t.text "restricted_visibility_levels"
+ t.integer "max_attachment_size", default: 10, null: false
end
create_table "broadcast_messages", force: true do |t|
@@ -131,6 +132,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do
t.string "title"
t.string "type"
t.string "fingerprint"
+ t.boolean "public", default: false, null: false
end
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
@@ -161,15 +163,20 @@ ActiveRecord::Schema.define(version: 20150324155957) do
t.integer "access_level", null: false
t.integer "source_id", null: false
t.string "source_type", null: false
- t.integer "user_id", null: false
+ t.integer "user_id"
t.integer "notification_level", null: false
t.string "type"
t.datetime "created_at"
t.datetime "updated_at"
+ t.integer "created_by_id"
+ t.string "invite_email"
+ t.string "invite_token"
+ t.datetime "invite_accepted_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
add_index "members", ["created_at", "id"], name: "index_members_on_created_at_and_id", using: :btree
+ add_index "members", ["invite_token"], name: "index_members_on_invite_token", unique: true, using: :btree
add_index "members", ["source_id", "source_type"], name: "index_members_on_source_id_and_source_type", using: :btree
add_index "members", ["type"], name: "index_members_on_type", using: :btree
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
@@ -316,6 +323,11 @@ ActiveRecord::Schema.define(version: 20150324155957) do
add_index "oauth_applications", ["owner_id", "owner_type"], name: "index_oauth_applications_on_owner_id_and_owner_type", using: :btree
add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree
+ create_table "project_import_data", force: true do |t|
+ t.integer "project_id"
+ t.text "data"
+ end
+
create_table "projects", force: true do |t|
t.string "name"
t.string "path"
@@ -458,6 +470,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do
t.integer "notification_level", default: 1, null: false
t.datetime "password_expires_at"
t.integer "created_by_id"
+ t.datetime "last_credential_check_at"
t.string "avatar"
t.string "confirmation_token"
t.datetime "confirmed_at"
@@ -465,7 +478,6 @@ ActiveRecord::Schema.define(version: 20150324155957) do
t.string "unconfirmed_email"
t.boolean "hide_no_ssh_key", default: false
t.string "website_url", default: "", null: false
- t.datetime "last_credential_check_at"
t.string "github_access_token"
t.string "gitlab_access_token"
t.string "notification_email"
@@ -474,6 +486,7 @@ ActiveRecord::Schema.define(version: 20150324155957) do
t.string "bitbucket_access_token"
t.string "bitbucket_access_token_secret"
t.string "location"
+ t.string "public_email", default: "", null: false
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 7fe244477db..971fe96fb8e 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -44,6 +44,10 @@ Parameters:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
"web_url": "http://example.com/diaspora/diaspora-client",
+ "tag_list": [
+ "example",
+ "disapora client"
+ ],
"owner": {
"id": 3,
"name": "Diaspora",
@@ -59,6 +63,7 @@ Parameters:
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
+ "creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
@@ -80,6 +85,10 @@ Parameters:
"ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
"http_url_to_repo": "http://example.com/brightbox/puppet.git",
"web_url": "http://example.com/brightbox/puppet",
+ "tag_list": [
+ "example",
+ "puppet"
+ ],
"owner": {
"id": 4,
"name": "Brightbox",
@@ -95,6 +104,7 @@ Parameters:
"snippets_enabled": false,
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13:46:02Z",
"description": "",
@@ -163,6 +173,10 @@ Parameters:
"ssh_url_to_repo": "git@example.com:diaspora/diaspora-project-site.git",
"http_url_to_repo": "http://example.com/diaspora/diaspora-project-site.git",
"web_url": "http://example.com/diaspora/diaspora-project-site",
+ "tag_list": [
+ "example",
+ "disapora project"
+ ],
"owner": {
"id": 3,
"name": "Diaspora",
@@ -178,6 +192,7 @@ Parameters:
"snippets_enabled": false,
"created_at": "2013-09-30T13: 46: 02Z",
"last_activity_at": "2013-09-30T13: 46: 02Z",
+ "creator_id": 3,
"namespace": {
"created_at": "2013-09-30T13: 46: 02Z",
"description": "",
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index ddc0c8eac2b..aa65a082a53 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,5 +1,36 @@
# Issue closing pattern
-By default you can close issues from commit messages by saying 'Closes #12' or 'Fixed #101'.
+If a commit message matches the regular expression below, all issues referenced from
+the matched text will be closed. This happens when the commit is pushed or merged
+into the default branch of a project.
-If you want to customize the message please do so in [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/73b92f85bcd6c213b845cc997843a969cf0906cf/config/gitlab.yml.example#L73)
+When not specified, the default issue_closing_pattern as shown below will be used:
+
+```bash
+((?:[Cc]los(?:e[sd]?|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)
+```
+
+For example:
+
+```
+git commit -m "Awesome commit message (Fix #20, Fixes #21 and Closes #22). This commit is also related to #17 and fixes #18, #19 and #23."
+```
+
+will close `#20`, `#21`, `#22`, `#18`, `#19` and `#23`, but `#17` won't be closed
+as it does not match the pattern. It also works with multiline commit messages.
+
+Tip: you can test this closing pattern at [http://rubular.com][1]. Use this site
+to test your own patterns.
+
+## Change the pattern
+
+For Omnibus installs you can change the default pattern in `/etc/gitlab/gitlab.rb`:
+
+```
+issue_closing_pattern: '((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?#\d+(?:(?:, *| +and +)?))+)'
+```
+
+For manual installs you can customize the pattern in [gitlab.yml][0].
+
+[0]: https://gitlab.com/gitlab-org/gitlab-ce/blob/40c3675372320febf5264061c9bcd63db2dfd13c/config/gitlab.yml.example#L65
+[1]: http://rubular.com/r/Xmbexed1OJ
diff --git a/doc/install/installation.md b/doc/install/installation.md
index d6208bb0797..a61a40ebd16 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -103,8 +103,8 @@ Remove the old Ruby 1.8 if present
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz
- cd ruby-2.1.5
+ curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
+ cd ruby-2.1.6
./configure --disable-install-rdoc
make
sudo make install
@@ -183,9 +183,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-9-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-10-stable gitlab
-**Note:** You can change `7-9-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `7-10-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -280,7 +280,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
GitLab Shell is an SSH access and repository management software developed specially for GitLab.
# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed):
- sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.0] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.2] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
# By default, the gitlab-shell config is generated from your main GitLab config.
# You can review (and modify) the gitlab-shell config as follows:
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 125ce31b521..b67f793c591 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -51,6 +51,11 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
# disable this setting, because the userPrincipalName contains an '@'.
allow_username_or_email_login: false
+ # To maintain tight control over the number of active users on your GitLab installation,
+ # enable this setting to keep new users blocked until they have been cleared by the admin
+ # (default: false).
+ block_auto_created_users: false
+
# Base where we can search for users
#
# Ex. ou=People,dc=gitlab,dc=example
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index 965d8fc313f..1d5fd4c8b0d 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -421,7 +421,7 @@ Quote break.
You can also use raw HTML in your Markdown, and it'll mostly work pretty well.
-Note that inline HTML is disabled in the default Gitlab configuration, although it is [possible](https://github.com/gitlabhq/gitlabhq/pull/8007/commits) for the system administrator to enable it.
+See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes.
```no-highlight
<dl>
@@ -441,8 +441,6 @@ Note that inline HTML is disabled in the default Gitlab configuration, although
<dd>Does *not* work **very** well. Use HTML <em>tags</em>.</dd>
</dl>
-See the documentation for HTML::Pipeline's [SanitizationFilter](http://www.rubydoc.info/gems/html-pipeline/HTML/Pipeline/SanitizationFilter#WHITELIST-constant) class for the list of allowed HTML tags and attributes. In addition to the default `SanitizationFilter` whitelist, GitLab allows the `class`, `id`, and `style` attributes.
-
## Horizontal Rule
```
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 7c5a6c04639..bd439f7c6f3 100644
--- a/doc/public_access/public_access.md
+++ b/doc/public_access/public_access.md
@@ -41,4 +41,4 @@ When visiting the public page of an user, you will only see listed projects whic
## Restricting the use of public or internal projects
-In [gitlab.yml](https://gitlab.com/gitlab-org/gitlab-ce/blob/dbd88d453b8e6c78a423fa7e692004b1db6ea069/config/gitlab.yml.example#L64) you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users.
+In the Admin area under Settings you can disable public projects or public and internal projects for the entire GitLab installation to prevent people making code public by accident. The restricted visibility settings do not apply to admin users.
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 99cdfff0ac6..2e41fad89e7 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -17,6 +17,13 @@ sudo gitlab-rake gitlab:backup:create
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
+Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
+uploads (attachments), repositories. Use a comma to specify several options at the same time.
+
+```
+sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
+```
+
Example output:
```
diff --git a/doc/release/patch.md b/doc/release/patch.md
index 68156ae9c0e..4c7b471785f 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -35,16 +35,13 @@ git clone git@dev.gitlab.org:gitlab/release-tools.git
cd release-tools
```
-Bump version in stable branch, create release tag and push to remotes:
+Bump all versions in stable branch, even if the changes affect only EE, CE, or CI. Since all the versions are synced now,
+it doesn't make sense to say upgrade CE to 7.2, EE to 7.3 and CI to 7.1.
-```
-bundle exec rake release["x.x.x"]
-```
-
-Or if you need to release only EE:
+Create release tag and push to remotes:
```
-CE=false be rake release['x.x.x']
+bundle exec rake release["x.x.x"]
```
### Release
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 66941521c2e..0acf92fbf54 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -68,6 +68,12 @@ You can't add the same deploy key twice with the 'New Deploy Key' option.
If you want to add the same key to another project, please enable it in the
list that says 'Deploy keys from projects available to you'. All the deploy
keys of all the projects you have access to are available. This project
-access can happen through being a direct member of the projecti, or through
+access can happen through being a direct member of the project, or through
a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more
information.
+
+## Applications
+
+### Eclipse
+
+How to add your ssh key to Eclipse: http://wiki.eclipse.org/EGit/User_Guide#Eclipse_SSH_Configuration
diff --git a/doc/update/6.6-to-6.7.md b/doc/update/6.6-to-6.7.md
index 5622a7001ed..b4298c93429 100644
--- a/doc/update/6.6-to-6.7.md
+++ b/doc/update/6.6-to-6.7.md
@@ -71,6 +71,9 @@ sudo -u git -H gzip /home/git/gitlab-shell/gitlab-shell.log.1
# Close access to gitlab-satellites for others
sudo chmod u+rwx,g=rx,o-rwx /home/git/gitlab-satellites
+
+# Add directory for uploads
+sudo -u git -H mkdir -p /home/git/gitlab/public/uploads
```
## 5. Start application
diff --git a/doc/update/6.x-or-7.x-to-7.9.md b/doc/update/6.x-or-7.x-to-7.10.md
index bd6eb6b211f..2ee9a07cee3 100644
--- a/doc/update/6.x-or-7.x-to-7.9.md
+++ b/doc/update/6.x-or-7.x-to-7.10.md
@@ -1,7 +1,7 @@
-# From 6.x or 7.x to 7.9
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.9.md) for the most up to date instructions.*
+# From 6.x or 7.x to 7.10
+*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.10.md) for the most up to date instructions.*
-This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.9.
+This allows you to upgrade any version of GitLab from 6.0 and up (including 7.0 and up) to 7.10.
## Global issue numbers
@@ -35,7 +35,7 @@ You can check which version you are running with `ruby -v`.
If you are you running Ruby 2.0.x, you do not need to upgrade ruby, but can consider doing so for performance reasons.
-If you are running Ruby 2.1.1 consider upgrading to 2.1.5, because of the high memory usage of Ruby 2.1.1.
+If you are running Ruby 2.1.1 consider upgrading to 2.1.6, because of the high memory usage of Ruby 2.1.1.
Install, update dependencies:
@@ -47,8 +47,8 @@ Download and compile Ruby:
```bash
mkdir /tmp/ruby && cd /tmp/ruby
-curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.5.tar.gz | tar xz
-cd ruby-2.1.5
+curl --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
+cd ruby-2.1.6
./configure --disable-install-rdoc
make
sudo make install
@@ -71,7 +71,7 @@ sudo -u git -H git checkout -- db/schema.rb # local changes will be restored aut
For GitLab Community Edition:
```bash
-sudo -u git -H git checkout 7-9-stable
+sudo -u git -H git checkout 7-10-stable
```
OR
@@ -79,7 +79,7 @@ OR
For GitLab Enterprise Edition:
```bash
-sudo -u git -H git checkout 7-9-stable-ee
+sudo -u git -H git checkout 7-10-stable-ee
```
## 4. Install additional packages
@@ -161,11 +161,11 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
TIP: to see what changed in `gitlab.yml.example` in this release use next command:
```
-git diff 6-0-stable:config/gitlab.yml.example 7-9-stable:config/gitlab.yml.example
+git diff 6-0-stable:config/gitlab.yml.example 7-10-stable:config/gitlab.yml.example
```
-* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/gitlab.yml.example but with your settings.
-* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/config/unicorn.rb.example but with your settings.
+* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/gitlab.yml.example but with your settings.
+* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/config/unicorn.rb.example but with your settings.
* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings.
* Copy rack attack middleware config
@@ -181,8 +181,8 @@ sudo cp lib/support/logrotate/gitlab /etc/logrotate.d/gitlab
### Change Nginx settings
-* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab but with your settings.
-* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-9-stable/lib/support/nginx/gitlab-ssl but with your settings.
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-10-stable/lib/support/nginx/gitlab-ssl but with your settings.
* A new `location /uploads/` section has been added that needs to have the same content as the existing `location @gitlab` section.
## 9. Start application
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 6e70235f5b8..7e996dc47d4 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,6 +1,7 @@
# Workflow
- [Feature branch workflow](workflow.md)
+- [Project forking workflow](forking_workflow.md)
- [Project Features](project_features.md)
- [Authorization for merge requests](authorization_for_merge_requests.md)
- [Groups](groups.md)
diff --git a/doc/workflow/forking/branch_select.png b/doc/workflow/forking/branch_select.png
new file mode 100644
index 00000000000..275f64d113b
--- /dev/null
+++ b/doc/workflow/forking/branch_select.png
Binary files differ
diff --git a/doc/workflow/forking/fork_button.png b/doc/workflow/forking/fork_button.png
new file mode 100644
index 00000000000..def4266476a
--- /dev/null
+++ b/doc/workflow/forking/fork_button.png
Binary files differ
diff --git a/doc/workflow/forking/groups.png b/doc/workflow/forking/groups.png
new file mode 100644
index 00000000000..3ac64b3c8e7
--- /dev/null
+++ b/doc/workflow/forking/groups.png
Binary files differ
diff --git a/doc/workflow/forking/merge_request.png b/doc/workflow/forking/merge_request.png
new file mode 100644
index 00000000000..2dc00ed08a1
--- /dev/null
+++ b/doc/workflow/forking/merge_request.png
Binary files differ
diff --git a/doc/workflow/forking_workflow.md b/doc/workflow/forking_workflow.md
new file mode 100644
index 00000000000..8edf7c6ab3d
--- /dev/null
+++ b/doc/workflow/forking_workflow.md
@@ -0,0 +1,36 @@
+# Project forking workflow
+
+Forking a project to your own namespace is useful if you have no write access to the project you want to contribute
+to. If you do have write access or can request it we recommend working together in the same repository since it is simpler.
+See our **[GitLab Flow](https://about.gitlab.com/2014/09/29/gitlab-flow/)** article for more information about using
+branches to work together.
+
+## Creating a fork
+
+In order to create a fork of a project, all you need to do is click on the fork button located on the top right side
+of the screen, close to the project's URL and right next to the stars button.
+
+![Fork button](forking/fork_button.png)
+
+Once you do that you'll be presented with a screen where you can choose the namespace to fork to. Only namespaces
+(groups and your own namespace) where you have write access to, will be shown. Click on the namespace to create your
+fork there.
+
+![Groups view](forking/groups.png)
+
+After the forking is done, you can start working on the newly created repository. There you will have full
+[Owner](../permissions/permissions.md) access, so you can set it up as you please.
+
+## Merging upstream
+
+Once you are ready to send your code back to the main project, you need to create a merge request. Choose your forked
+project's main branch as the source and the original project's main branch as the destination and create the merge request.
+
+![Selecting branches](forking/branch_select.png)
+
+You can then assign the merge request to someone to have them review your changes. Upon pressing the 'Accept Merge Request'
+button, your changes will be added to the repository and branch you're merging into.
+
+![New merge request](forking/merge_request.png)
+
+
diff --git a/docker/Dockerfile b/docker/Dockerfile
index b228a66832a..bb25bb677ca 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -11,7 +11,7 @@ RUN apt-get update -q \
# If the Omnibus package version below is outdated please contribute a merge request to update it.
# If you run GitLab Enterprise Edition point it to a location where you have downloaded it.
RUN TMP_FILE=$(mktemp); \
- wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.0-omnibus.2-1_amd64.deb \
+ wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab_7.9.2-omnibus-1_amd64.deb \
&& dpkg -i $TMP_FILE \
&& rm -f $TMP_FILE
diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature
new file mode 100644
index 00000000000..9df47eb51fd
--- /dev/null
+++ b/features/admin/deploy_keys.feature
@@ -0,0 +1,21 @@
+@admin
+Feature: Admin Deploy Keys
+ Background:
+ Given I sign in as an admin
+ And there are public deploy keys in system
+
+ Scenario: Deploy Keys list
+ When I visit admin deploy keys page
+ Then I should see all public deploy keys
+
+ Scenario: Deploy Keys show
+ When I visit admin deploy keys page
+ And I click on first deploy key
+ Then I should see deploy key details
+
+ Scenario: Deploy Keys new
+ When I visit admin deploy keys page
+ And I click 'New Deploy Key'
+ And I submit new deploy key
+ Then I should be on admin deploy keys page
+ And I should see newly created deploy key
diff --git a/features/dashboard/issues.feature b/features/dashboard/issues.feature
index 72627e43e05..99dad88a402 100644
--- a/features/dashboard/issues.feature
+++ b/features/dashboard/issues.feature
@@ -10,10 +10,12 @@ Feature: Dashboard Issues
Scenario: I should see assigned issues
Then I should see issues assigned to me
+ @javascript
Scenario: I should see authored issues
When I click "Authored by me" link
Then I should see issues authored by me
+ @javascript
Scenario: I should see all issues
When I click "All" link
Then I should see all issues
diff --git a/features/dashboard/merge_requests.feature b/features/dashboard/merge_requests.feature
index dcef1290e7e..4a2c997d707 100644
--- a/features/dashboard/merge_requests.feature
+++ b/features/dashboard/merge_requests.feature
@@ -10,10 +10,12 @@ Feature: Dashboard Merge Requests
Scenario: I should see assigned merge_requests
Then I should see merge requests assigned to me
+ @javascript
Scenario: I should see authored merge_requests
When I click "Authored by me" link
Then I should see merge requests authored by me
+ @javascript
Scenario: I should see all merge_requests
When I click "All" link
Then I should see all merge requests
diff --git a/features/groups.feature b/features/groups.feature
index 05546e0d6ef..415e43d6ae7 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -47,6 +47,21 @@ Feature: Groups
Then I should not see group "Owned" avatar
And I should not see the "Remove avatar" button
+ @javascript
+ Scenario: Add user to group
+ Given gitlab user "Mike"
+ When I visit group "Owned" members page
+ And I click link "Add members"
+ When I select "Mike" as "Reporter"
+ Then I should see "Mike" in team list as "Reporter"
+
+ @javascript
+ Scenario: Invite user to group
+ When I visit group "Owned" members page
+ And I click link "Add members"
+ When I select "sjobs@apple.com" as "Reporter"
+ Then I should see "sjobs@apple.com" in team list as invited "Reporter"
+
# Leave
@javascript
diff --git a/features/invites.feature b/features/invites.feature
new file mode 100644
index 00000000000..dc8eefaeaed
--- /dev/null
+++ b/features/invites.feature
@@ -0,0 +1,45 @@
+Feature: Invites
+ Background:
+ Given "John Doe" is owner of group "Owned"
+ And "John Doe" has invited "user@example.com" to group "Owned"
+
+ Scenario: Viewing invitation when signed out
+ When I visit the invitation page
+ Then I should be redirected to the sign in page
+ And I should see a notice telling me to sign in
+
+ Scenario: Signing in to view invitation
+ When I visit the invitation page
+ And I sign in as "Mary Jane"
+ Then I should be redirected to the invitation page
+
+ Scenario: Viewing invitation when signed in
+ Given I sign in as "Mary Jane"
+ And I visit the invitation page
+ Then I should see the invitation details
+ And I should see an "Accept invitation" button
+ And I should see a "Decline" button
+
+ Scenario: Viewing invitation as an existing member
+ Given I sign in as "John Doe"
+ And I visit the invitation page
+ Then I should see a message telling me I'm already a member
+
+ Scenario: Accepting the invitation
+ Given I sign in as "Mary Jane"
+ And I visit the invitation page
+ And I click the "Accept invitation" button
+ Then I should be redirected to the group page
+ And I should see a notice telling me I have access
+
+ Scenario: Declining the application when signed in
+ Given I sign in as "Mary Jane"
+ And I visit the invitation page
+ And I click the "Decline" button
+ Then I should be redirected to the dashboard
+ And I should see a notice telling me I have declined
+
+ Scenario: Declining the application when signed out
+ When I visit the invitation's decline page
+ Then I should be redirected to the sign in page
+ And I should see a notice telling me I have declined
diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature
index 13e3b9bbd2e..a71f6124d9c 100644
--- a/features/project/deploy_keys.feature
+++ b/features/project/deploy_keys.feature
@@ -6,7 +6,17 @@ Feature: Project Deploy Keys
Scenario: I should see deploy keys list
Given project has deploy key
When I visit project deploy keys page
- Then I should see project deploy keys
+ Then I should see project deploy key
+
+ Scenario: I should see project deploy keys
+ Given other project has deploy key
+ When I visit project deploy keys page
+ Then I should see other project deploy key
+
+ Scenario: I should see public deploy keys
+ Given public deploy key exists
+ When I visit project deploy keys page
+ Then I should see public deploy key
Scenario: I add new deploy key
Given I visit project deploy keys page
@@ -15,9 +25,16 @@ Feature: Project Deploy Keys
Then I should be on deploy keys page
And I should see newly created deploy key
- Scenario: I attach deploy key to project
+ Scenario: I attach other project deploy key to project
Given other project has deploy key
And I visit project deploy keys page
When I click attach deploy key
Then I should be on deploy keys page
And I should see newly created deploy key
+
+ Scenario: I attach public deploy key to project
+ Given public deploy key exists
+ And I visit project deploy keys page
+ When I click attach deploy key
+ Then I should be on deploy keys page
+ And I should see newly created deploy key
diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature
index 2c69a78a749..e316f519861 100644
--- a/features/project/issues/filter_labels.feature
+++ b/features/project/issues/filter_labels.feature
@@ -8,11 +8,7 @@ Feature: Project Issues Filter Labels
And project "Shop" has issue "Feature1" with labels: "feature"
Given I visit project "Shop" issues page
- Scenario: I should see project issues
- Then I should see "bug" in labels filter
- And I should see "feature" in labels filter
- And I should see "enhancement" in labels filter
-
+ @javascript
Scenario: I filter by one label
Given I click link "bug"
Then I should see "Bugfix1" in issues list
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index b9031f6f32b..eb813884d1e 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -25,6 +25,12 @@ Feature: Project Issues
Given I click link "Release 0.4"
Then I should see issue "Release 0.4"
+ @javascript
+ Scenario: I visit issue page
+ Given I add a user to project "Shop"
+ And I click "author" dropdown
+ Then I see current user as the first user
+
Scenario: I submit new unassigned issue
Given I click link "New Issue"
And I submit new issue "500 error on profile"
@@ -42,6 +48,7 @@ Feature: Project Issues
Given I visit issue page "Release 0.4"
And I leave a comment like "XML attached"
Then I should see comment "XML attached"
+ And I should see an error alert section within the comment form
@javascript
Scenario: I search issue
diff --git a/features/project/star.feature b/features/project/star.feature
index 3322f891805..a45f9c470ea 100644
--- a/features/project/star.feature
+++ b/features/project/star.feature
@@ -13,7 +13,7 @@ Feature: Project Star
Given public project "Community"
And I visit project "Community" page
When I click on the star toggle button
- Then The project has 0 stars
+ Then I redirected to sign in page
@javascript
Scenario: Signed in users can toggle star
diff --git a/features/project/team_management.feature b/features/project/team_management.feature
index 22393622bb9..6cda225ea7b 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -18,6 +18,12 @@ Feature: Project Team Management
Then I should see "Mike" in team list as "Reporter"
@javascript
+ Scenario: Invite user to project
+ Given I click link "Add members"
+ And I select "sjobs@apple.com" as "Reporter"
+ Then I should see "sjobs@apple.com" in team list as invited "Reporter"
+
+ @javascript
Scenario: Update user access
Given I should see "Sam" in team list as "Developer"
And I change "Sam" role to "Reporter"
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index 4a8c771ddac..977cd609a11 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -62,3 +62,27 @@ Feature: Project Wiki
And I browse to wiki page with images
And I click on image link
Then I should see the new wiki page form
+
+ @javascript
+ Scenario: New Wiki page that has a path
+ Given I create a New page with paths
+ And I click on the "Pages" button
+ Then I should see non-escaped link in the pages list
+
+ @javascript
+ Scenario: Edit Wiki page that has a path
+ Given I create a New page with paths
+ And I click on the "Pages" button
+ And I edit the Wiki page with a path
+ Then I should see a non-escaped path
+ And I should see the Editing page
+ And I change the content
+ Then I should see the updated content
+
+ @javascript
+ Scenario: View the page history of a Wiki page that has a path
+ Given I create a New page with paths
+ And I click on the "Pages" button
+ And I view the page history of a Wiki page that has a path
+ Then I should see a non-escaped path
+ And I should see the page history
diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb
new file mode 100644
index 00000000000..fb0b611762e
--- /dev/null
+++ b/features/steps/admin/deploy_keys.rb
@@ -0,0 +1,57 @@
+class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedAdmin
+
+ step 'there are public deploy keys in system' do
+ create(:deploy_key, public: true)
+ create(:another_deploy_key, public: true)
+ end
+
+ step 'I should see all public deploy keys' do
+ DeployKey.are_public.each do |p|
+ page.should have_content p.title
+ end
+ end
+
+ step 'I click on first deploy key' do
+ click_link DeployKey.are_public.first.title
+ end
+
+ step 'I should see deploy key details' do
+ deploy_key = DeployKey.are_public.first
+ current_path.should == admin_deploy_key_path(deploy_key)
+ page.should have_content(deploy_key.title)
+ page.should have_content(deploy_key.key)
+ end
+
+ step 'I visit admin deploy key page' do
+ visit admin_deploy_key_path(deploy_key)
+ end
+
+ step 'I visit admin deploy keys page' do
+ visit admin_deploy_keys_path
+ end
+
+ step 'I click \'New Deploy Key\'' do
+ click_link 'New Deploy Key'
+ end
+
+ step 'I submit new deploy key' do
+ fill_in "deploy_key_title", with: "laptop"
+ fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
+ click_button "Create"
+ end
+
+ step 'I should be on admin deploy keys page' do
+ current_path.should == admin_deploy_keys_path
+ end
+
+ step 'I should see newly created deploy key' do
+ page.should have_content(deploy_key.title)
+ end
+
+ def deploy_key
+ @deploy_key ||= DeployKey.are_public.first
+ end
+end
diff --git a/features/steps/dashboard/issues.rb b/features/steps/dashboard/issues.rb
index b77113e3974..60da36e86de 100644
--- a/features/steps/dashboard/issues.rb
+++ b/features/steps/dashboard/issues.rb
@@ -1,6 +1,7 @@
class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
+ include Select2Helper
step 'I should see issues assigned to me' do
should_see(assigned_issue)
@@ -35,21 +36,13 @@ class Spinach::Features::DashboardIssues < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- within ".assignee-filter" do
- click_link "Any"
- end
- within ".author-filter" do
- click_link current_user.name
- end
+ select2(current_user.id, from: "#author_id")
+ select2(nil, from: "#assignee_id")
end
step 'I click "All" link' do
- within ".author-filter" do
- click_link "Any"
- end
- within ".assignee-filter" do
- click_link "Any"
- end
+ select2(nil, from: "#author_id")
+ select2(nil, from: "#assignee_id")
end
def should_see(issue)
diff --git a/features/steps/dashboard/merge_requests.rb b/features/steps/dashboard/merge_requests.rb
index 6261c89924c..9d92082bb83 100644
--- a/features/steps/dashboard/merge_requests.rb
+++ b/features/steps/dashboard/merge_requests.rb
@@ -1,6 +1,7 @@
class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
include SharedAuthentication
include SharedPaths
+ include Select2Helper
step 'I should see merge requests assigned to me' do
should_see(assigned_merge_request)
@@ -39,21 +40,13 @@ class Spinach::Features::DashboardMergeRequests < Spinach::FeatureSteps
end
step 'I click "Authored by me" link' do
- within ".assignee-filter" do
- click_link "Any"
- end
- within ".author-filter" do
- click_link current_user.name
- end
+ select2(current_user.id, from: "#author_id")
+ select2(nil, from: "#assignee_id")
end
step 'I click "All" link' do
- within ".author-filter" do
- click_link "Any"
- end
- within ".assignee-filter" do
- click_link "Any"
- end
+ select2(nil, from: "#author_id")
+ select2(nil, from: "#assignee_id")
end
def should_see(merge_request)
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 91921f5e21c..228b83e5fd0 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -5,6 +5,49 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include SharedUser
include Select2Helper
+ step 'gitlab user "Mike"' do
+ create(:user, name: "Mike")
+ end
+
+ step 'I click link "Add members"' do
+ find(:css, 'button.btn-new').click
+ end
+
+ step 'I select "Mike" as "Reporter"' do
+ user = User.find_by(name: "Mike")
+
+ within ".users-group-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see "Mike" in team list as "Reporter"' do
+ within '.well-list' do
+ page.should have_content('Mike')
+ page.should have_content('Reporter')
+ end
+ end
+
+ step 'I select "sjobs@apple.com" as "Reporter"' do
+ within ".users-group-form" do
+ select2("sjobs@apple.com", from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+
+ click_button "Add users to group"
+ end
+
+ step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
+ within '.well-list' do
+ page.should have_content('sjobs@apple.com')
+ page.should have_content('invited')
+ page.should have_content('Reporter')
+ end
+ end
+
step 'I should see group "Owned" projects list' do
Group.find_by(name: "Owned").projects.each do |project|
page.should have_link project.name
diff --git a/features/steps/invites.rb b/features/steps/invites.rb
new file mode 100644
index 00000000000..d051cc3edc8
--- /dev/null
+++ b/features/steps/invites.rb
@@ -0,0 +1,80 @@
+class Spinach::Features::Invites < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedUser
+ include SharedGroup
+
+ step '"John Doe" has invited "user@example.com" to group "Owned"' do
+ user = User.find_by(name: "John Doe")
+ group = Group.find_by(name: "Owned")
+ group.add_user("user@example.com", Gitlab::Access::DEVELOPER, user)
+ end
+
+ step 'I visit the invitation page' do
+ group = Group.find_by(name: "Owned")
+ invite = group.group_members.invite.last
+ invite.generate_invite_token!
+ @raw_invite_token = invite.raw_invite_token
+ visit invite_path(@raw_invite_token)
+ end
+
+ step 'I should be redirected to the sign in page' do
+ expect(current_path).to eq(new_user_session_path)
+ end
+
+ step 'I should see a notice telling me to sign in' do
+ expect(page).to have_content "To accept this invitation, sign in"
+ end
+
+ step 'I should be redirected to the invitation page' do
+ expect(current_path).to eq(invite_path(@raw_invite_token))
+ end
+
+ step 'I should see the invitation details' do
+ expect(page).to have_content("You have been invited by John Doe to join group Owned as Developer.")
+ end
+
+ step "I should see a message telling me I'm already a member" do
+ expect(page).to have_content("However, you are already a member of this group.")
+ end
+
+ step 'I should see an "Accept invitation" button' do
+ expect(page).to have_link("Accept invitation")
+ end
+
+ step 'I should see a "Decline" button' do
+ expect(page).to have_link("Decline")
+ end
+
+ step 'I click the "Accept invitation" button' do
+ page.click_link "Accept invitation"
+ end
+
+ step 'I should be redirected to the group page' do
+ group = Group.find_by(name: "Owned")
+ expect(current_path).to eq(group_path(group))
+ end
+
+ step 'I should see a notice telling me I have access' do
+ expect(page).to have_content("You have been granted Developer access to group Owned.")
+ end
+
+ step 'I click the "Decline" button' do
+ page.click_link "Decline"
+ end
+
+ step 'I should be redirected to the dashboard' do
+ expect(current_path).to eq(dashboard_path)
+ end
+
+ step 'I should see a notice telling me I have declined' do
+ expect(page).to have_content("You have declined the invitation to join group Owned.")
+ end
+
+ step "I visit the invitation's decline page" do
+ group = Group.find_by(name: "Owned")
+ invite = group.group_members.invite.last
+ invite.generate_invite_token!
+ @raw_invite_token = invite.raw_invite_token
+ visit decline_invite_path(@raw_invite_token)
+ end
+end
diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb
index 4bf5cb5fa40..50e14513a7a 100644
--- a/features/steps/project/deploy_keys.rb
+++ b/features/steps/project/deploy_keys.rb
@@ -7,12 +7,24 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
create(:deploy_keys_project, project: @project)
end
- step 'I should see project deploy keys' do
+ step 'I should see project deploy key' do
within '.enabled-keys' do
page.should have_content deploy_key.title
end
end
+ step 'I should see other project deploy key' do
+ within '.available-keys' do
+ page.should have_content other_deploy_key.title
+ end
+ end
+
+ step 'I should see public deploy key' do
+ within '.available-keys' do
+ page.should have_content public_deploy_key.title
+ end
+ end
+
step 'I click \'New Deploy Key\'' do
click_link 'New Deploy Key'
end
@@ -39,6 +51,10 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
create(:deploy_keys_project, project: @second_project)
end
+ step 'public deploy key exists' do
+ create(:deploy_key, public: true)
+ end
+
step 'I click attach deploy key' do
within '.available-keys' do
click_link 'Enable'
@@ -50,4 +66,12 @@ class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps
def deploy_key
@project.deploy_keys.last
end
+
+ def other_deploy_key
+ @second_project.deploy_keys.last
+ end
+
+ def public_deploy_key
+ DeployKey.are_public.last
+ end
end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index e62fa9c84c8..5740bd12837 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -2,24 +2,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
-
- step 'I should see "bug" in labels filter' do
- within ".labels-filter" do
- page.should have_content "bug"
- end
- end
-
- step 'I should see "feature" in labels filter' do
- within ".labels-filter" do
- page.should have_content "feature"
- end
- end
-
- step 'I should see "enhancement" in labels filter' do
- within ".labels-filter" do
- page.should have_content "enhancement"
- end
- end
+ include Select2Helper
step 'I should see "Bugfix1" in issues list' do
within ".issues-list" do
@@ -46,9 +29,7 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
end
step 'I click link "bug"' do
- within ".labels-filter" do
- click_link "bug"
- end
+ select2('bug', from: "#label_name")
end
step 'I click link "feature"' do
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index e8ca3f7c176..b8e282b2029 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -59,6 +59,18 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
click_link "New Issue"
end
+ step 'I click "author" dropdown' do
+ first('.ajax-users-select').click
+ end
+
+ step 'I see current user as the first user' do
+ expect(page).to have_selector('.user-result', visible: true, count: 4)
+ users = page.all('.user-name')
+ users[0].text.should == 'Any'
+ users[1].text.should == 'Unassigned'
+ users[2].text.should == current_user.name
+ end
+
step 'I submit new issue "500 error on profile"' do
fill_in "issue_title", with: "500 error on profile"
click_button "Submit new issue"
@@ -204,6 +216,12 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
end
+ step 'I should see an error alert section within the comment form' do
+ within(".js-main-target-form") do
+ find(".error-alert")
+ end
+ end
+
step 'The code block should be unchanged' do
page.should have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```")
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 40c102833a4..bb1f9f129c0 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -61,8 +61,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see that I am unsubscribed' do
- sleep 0.2
- find(".subscribe-button span").text.should == "Subscribe"
+ find(".subscribe-button span").should have_content("Subscribe")
end
step 'I click button "Unsubscribe"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 557555aee58..caf6c73ee06 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the new file name with an illegal name' do
- fill_in :file_name, with: '.git'
+ fill_in :file_name, with: 'Spaces Not Allowed'
end
step 'I fill the commit message' do
diff --git a/features/steps/project/star.rb b/features/steps/project/star.rb
index ae2e4c7a201..50cdfd73c34 100644
--- a/features/steps/project/star.rb
+++ b/features/steps/project/star.rb
@@ -22,12 +22,16 @@ class Spinach::Features::ProjectStar < Spinach::FeatureSteps
# Requires @javascript
step "I click on the star toggle button" do
- find(".star .toggle", visible: true).click
+ find(".star-btn", visible: true).click
+ end
+
+ step 'I redirected to sign in page' do
+ current_path.should == new_user_session_path
end
protected
def has_n_stars(n)
- expect(page).to have_css(".star .count", text: /^#{n}$/, visible: true)
+ expect(page).to have_css(".star-btn .count", text: n, visible: true)
end
end
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index 0eefe2b5688..e95621071c4 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -35,6 +35,22 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
end
+ step 'I select "sjobs@apple.com" as "Reporter"' do
+ within ".users-project-form" do
+ select2("sjobs@apple.com", from: "#user_ids", multiple: true)
+ select "Reporter", from: "access_level"
+ end
+ click_button "Add users to project"
+ end
+
+ step 'I should see "sjobs@apple.com" in team list as invited "Reporter"' do
+ within ".access-reporter" do
+ page.should have_content('sjobs@apple.com')
+ page.should have_content('invited')
+ page.should have_content('Reporter')
+ end
+ end
+
step 'I should see "Sam" in team list as "Developer"' do
within ".access-developer" do
page.should have_content('Sam')
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index cd7d5eac243..bb93e582a1f 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -3,6 +3,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
include SharedProject
include SharedNote
include SharedPaths
+ include WikiHelper
step 'I click on the Cancel button' do
within(:css, ".form-actions") do
@@ -123,6 +124,41 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
page.should have_content('Editing - image.jpg')
end
+ step 'I create a New page with paths' do
+ click_on 'New Page'
+ fill_in 'Page slug', with: 'one/two/three'
+ click_on 'Build'
+ fill_in "wiki_content", with: 'wiki content'
+ click_on "Create page"
+ current_path.should include 'one/two/three'
+ end
+
+ step 'I should see non-escaped link in the pages list' do
+ page.should have_xpath("//a[@href='/#{project.path_with_namespace}/wikis/one/two/three']")
+ end
+
+ step 'I edit the Wiki page with a path' do
+ click_on 'three'
+ click_on 'Edit'
+ end
+
+ step 'I should see a non-escaped path' do
+ current_path.should include 'one/two/three'
+ end
+
+ step 'I should see the Editing page' do
+ page.should have_content('Editing')
+ end
+
+ step 'I view the page history of a Wiki page that has a path' do
+ click_on 'three'
+ click_on 'Page History'
+ end
+
+ step 'I should see the page history' do
+ page.should have_content('History for')
+ end
+
def wiki
@project_wiki = ProjectWiki.new(project, current_user)
end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 41f71ae29cb..b60ac5e3423 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -14,6 +14,13 @@ module SharedProject
@project.team << [@user, :master]
end
+ # Add another user to project "Shop"
+ step 'I add a user to project "Shop"' do
+ @project = Project.find_by(name: "Shop")
+ other_user = create(:user, name: 'Alpha')
+ @project.team << [other_user, :master]
+ end
+
# Create another specific project called "Forum"
step 'I own project "Forum"' do
@project = Project.find_by(name: "Forum")
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index edfdf842f85..592100a7045 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -1,5 +1,4 @@
require 'mime/types'
-require 'uri'
module API
# Projects API
@@ -101,10 +100,11 @@ module API
# branch (required) - The name of the branch
# Example Request:
# DELETE /projects/:id/repository/branches/:branch
- delete ":id/repository/branches/:branch" do
+ delete ":id/repository/branches/:branch",
+ requirements: { branch: /.*/ } do
authorize_push_project
result = DeleteBranchService.new(user_project, current_user).
- execute(URI.unescape(params[:branch]))
+ execute(params[:branch])
if result[:status] == :success
{
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 489be210784..36332bc6514 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -46,7 +46,7 @@ module API
end
class Project < Grape::Entity
- expose :id, :description, :default_branch
+ expose :id, :description, :default_branch, :tag_list
expose :public?, as: :public
expose :archived?, as: :archived
expose :visibility_level, :ssh_url_to_repo, :http_url_to_repo, :web_url
@@ -54,6 +54,7 @@ module API
expose :name, :name_with_namespace
expose :path, :path_with_namespace
expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :snippets_enabled, :created_at, :last_activity_at
+ expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::ForkedFromProject, if: lambda{ | project, options | project.forked? }
expose :avatar_url
diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb
index ed54c7f6ff0..ab9b7c602b5 100644
--- a/lib/api/group_members.rb
+++ b/lib/api/group_members.rb
@@ -9,8 +9,7 @@ module API
# GET /groups/:id/members
get ":id/members" do
group = find_group(params[:id])
- members = group.group_members
- users = (paginate members).collect(&:user)
+ users = group.users
present users, with: Entities::GroupMember, group: group
end
@@ -24,7 +23,7 @@ module API
# POST /groups/:id/members
post ":id/members" do
group = find_group(params[:id])
- authorize! :manage_group, group
+ authorize! :admin_group, group
required_attributes! [:user_id, :access_level]
unless validate_access_level?(params[:access_level])
@@ -35,7 +34,7 @@ module API
render_api_error!("Already exists", 409)
end
- group.add_users([params[:user_id]], params[:access_level])
+ group.add_users([params[:user_id]], params[:access_level], current_user)
member = group.group_members.find_by(user_id: params[:user_id])
present member.user, with: Entities::GroupMember, group: group
end
@@ -50,7 +49,7 @@ module API
# PUT /groups/:id/members/:user_id
put ':id/members/:user_id' do
group = find_group(params[:id])
- authorize! :manage_group, group
+ authorize! :admin_group, group
required_attributes! [:access_level]
group_member = group.group_members.find_by(user_id: params[:user_id])
@@ -74,7 +73,7 @@ module API
# DELETE /groups/:id/members/:user_id
delete ":id/members/:user_id" do
group = find_group(params[:id])
- authorize! :manage_group, group
+ authorize! :admin_group, group
member = group.group_members.find_by(user_id: params[:user_id])
if member.nil?
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index a92abd4b690..8cb9f920975 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -61,7 +61,7 @@ module API
# DELETE /groups/:id
delete ":id" do
group = find_group(params[:id])
- authorize! :manage_group, group
+ authorize! :admin_group, group
group.destroy
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 83f65eec6cc..e3fff79d68f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -88,17 +88,14 @@ module API
present user_project, with: Entities::ProjectWithAccess, user: current_user
end
- # Get a single project events
+ # Get events for a single project
#
# Parameters:
# id (required) - The ID of a project
# Example Request:
# GET /projects/:id/events
get ":id/events" do
- limit = (params[:per_page] || 20).to_i
- offset = (params[:page] || 0).to_i * limit
- events = user_project.events.recent.limit(limit).offset(offset)
-
+ events = paginate user_project.events.recent
present events, with: Entities::Event
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index b259914a01c..1fbf3dca3c6 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -133,10 +133,11 @@ module API
authorize! :download_code, user_project
begin
- file_path = ArchiveRepositoryService.new.execute(
- user_project,
- params[:sha],
- params[:format])
+ file_path = ArchiveRepositoryService.new(
+ user_project,
+ params[:sha],
+ params[:format]
+ ).execute
rescue
not_found!('File')
end
@@ -149,7 +150,7 @@ module API
env['api.format'] = :binary
present data
else
- not_found!('File')
+ redirect request.fullpath
end
end
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index afd05897509..b69aebf9fe1 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -1,7 +1,5 @@
module Backup
class Manager
- BACKUP_CONTENTS = %w{repositories/ db/ uploads/ backup_information.yml}
-
def pack
# saving additional informations
s = {}
@@ -9,6 +7,7 @@ module Backup
s[:backup_created_at] = Time.now
s[:gitlab_version] = Gitlab::VERSION
s[:tar_version] = tar_version
+ s[:skipped] = ENV["SKIP"]
tar_file = "#{s[:backup_created_at].to_i}_gitlab_backup.tar"
Dir.chdir(Gitlab.config.backup.path) do
@@ -17,12 +16,12 @@ module Backup
file << s.to_yaml.gsub(/^---\n/,'')
end
- FileUtils.chmod_R(0700, %w{db uploads repositories})
+ FileUtils.chmod(0700, folders_to_backup)
# create archive
$progress.print "Creating backup archive: #{tar_file} ... "
orig_umask = File.umask(0077)
- if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS)
+ if Kernel.system('tar', '-cf', tar_file, *backup_contents)
$progress.puts "done".green
else
puts "creating archive #{tar_file} failed".red
@@ -46,6 +45,7 @@ module Backup
connection = ::Fog::Storage.new(connection_settings)
directory = connection.directories.get(remote_directory)
+
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false)
$progress.puts "done".green
else
@@ -56,7 +56,10 @@ module Backup
def cleanup
$progress.print "Deleting tmp directories ... "
- BACKUP_CONTENTS.each do |dir|
+
+ backup_contents.each do |dir|
+ next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
+
if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
$progress.puts "done".green
else
@@ -73,6 +76,7 @@ module Backup
if keep_time > 0
removed = 0
+
Dir.chdir(Gitlab.config.backup.path) do
file_list = Dir.glob('*_gitlab_backup.tar')
file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ }
@@ -84,6 +88,7 @@ module Backup
end
end
end
+
$progress.puts "done. (#{removed} removed)".green
else
$progress.puts "skipping".yellow
@@ -96,6 +101,7 @@ module Backup
# check for existing backups in the backup dir
file_list = Dir.glob("*_gitlab_backup.tar").each.map { |f| f.split(/_/).first.to_i }
puts "no backups found" if file_list.count == 0
+
if file_list.count > 1 && ENV["BACKUP"].nil?
puts "Found more than one backup, please specify which one you want to restore:"
puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
@@ -110,6 +116,7 @@ module Backup
end
$progress.print "Unpacking backup ... "
+
unless Kernel.system(*%W(tar -xf #{tar_file}))
puts "unpacking backup failed".red
exit 1
@@ -117,7 +124,6 @@ module Backup
$progress.puts "done".green
end
- settings = YAML.load_file("backup_information.yml")
ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
# restoring mismatching backups can lead to unexpected problems
@@ -136,5 +142,29 @@ module Backup
tar_version, _ = Gitlab::Popen.popen(%W(tar --version))
tar_version.force_encoding('locale').split("\n").first
end
+
+ def skipped?(item)
+ settings[:skipped] && settings[:skipped].include?(item)
+ end
+
+ private
+
+ def backup_contents
+ folders_to_backup + ["backup_information.yml"]
+ end
+
+ def folders_to_backup
+ folders = %w{repositories db uploads}
+
+ if ENV["SKIP"]
+ return folders.reject{ |folder| ENV["SKIP"].include?(folder) }
+ end
+
+ folders
+ end
+
+ def settings
+ @settings ||= YAML.load_file("backup_information.yml")
+ end
end
end
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 42970c1be59..2eae55e534b 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -25,8 +25,8 @@ class FileSizeValidator < ActiveModel::EachValidator
keys.each do |key|
value = options[key]
- unless value.is_a?(Integer) && value >= 0
- raise ArgumentError, ":#{key} must be a nonnegative Integer"
+ unless (value.is_a?(Integer) && value >= 0) || value.is_a?(Symbol)
+ raise ArgumentError, ":#{key} must be a nonnegative Integer or symbol"
end
end
end
@@ -39,6 +39,14 @@ class FileSizeValidator < ActiveModel::EachValidator
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
+ check_value =
+ case check_value
+ when Integer
+ check_value
+ when Symbol
+ record.send(check_value)
+ end
+
value ||= [] if key == :maximum
value_size = value.size
diff --git a/lib/gitlab.rb b/lib/gitlab.rb
new file mode 100644
index 00000000000..5fc1862c3e9
--- /dev/null
+++ b/lib/gitlab.rb
@@ -0,0 +1,5 @@
+require 'gitlab/git'
+
+module Gitlab
+ autoload :Satellite, 'gitlab/satellite/satellite'
+end
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index aabc7f1e69a..530f9d93de4 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -240,7 +240,7 @@ module Gitlab
gitlab_shell_version_file = "#{gitlab_shell_path}/VERSION"
if File.readable?(gitlab_shell_version_file)
- File.read(gitlab_shell_version_file)
+ File.read(gitlab_shell_version_file).chomp
end
end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index db33af2c2da..54420e62c90 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -10,29 +10,16 @@ module Gitlab
end
def execute
- @project = Project.new(
+ ::Projects::CreateService.new(current_user,
name: repo["name"],
path: repo["slug"],
description: repo["description"],
- namespace: namespace,
- creator: current_user,
+ namespace_id: namespace.id,
visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
import_type: "bitbucket",
import_source: "#{repo["owner"]}/#{repo["slug"]}",
import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git"
- )
-
- if @project.save!
- @project.reload
-
- if @project.import_failed?
- @project.import_retry
- else
- @project.import_start
- end
- end
-
- @project
+ ).execute
end
end
end
diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb
index a9fd59f03d9..ab184d95c05 100644
--- a/lib/gitlab/closing_issue_extractor.rb
+++ b/lib/gitlab/closing_issue_extractor.rb
@@ -1,21 +1,20 @@
module Gitlab
- module ClosingIssueExtractor
+ class ClosingIssueExtractor
ISSUE_CLOSING_REGEX = Regexp.new(Gitlab.config.gitlab.issue_closing_pattern)
- def self.closed_by_message_in_project(message, project)
- issues = []
+ def initialize(project, current_user = nil)
+ @extractor = Gitlab::ReferenceExtractor.new(project, current_user)
+ end
- unless message.nil?
- md = message.scan(ISSUE_CLOSING_REGEX)
+ def closed_by_message(message)
+ return [] if message.nil?
+
+ closing_statements = message.scan(ISSUE_CLOSING_REGEX).
+ map { |ref| ref[0] }.join(" ")
- md.each do |ref|
- extractor = Gitlab::ReferenceExtractor.new
- extractor.analyze(ref[0], project)
- issues += extractor.issues_for(project)
- end
- end
+ @extractor.analyze(closing_statements)
- issues.uniq
+ @extractor.issues
end
end
end
diff --git a/lib/gitlab/contributors.rb b/lib/gitlab/contributor.rb
index c41e92b620f..c41e92b620f 100644
--- a/lib/gitlab/contributors.rb
+++ b/lib/gitlab/contributor.rb
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 0ebebfa09c4..d8f696d247b 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -20,7 +20,8 @@ module Gitlab
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
sign_in_text: Settings.extra['sign_in_text'],
- restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels']
+ restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
+ max_attachment_size: Settings.gitlab['max_attachment_size']
)
end
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 9439ca6cbf4..2723eec933e 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -10,29 +10,16 @@ module Gitlab
end
def execute
- @project = Project.new(
+ ::Projects::CreateService.new(current_user,
name: repo.name,
path: repo.name,
description: repo.description,
- namespace: namespace,
- creator: current_user,
+ namespace_id: namespace.id,
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://#{current_user.github_access_token}@")
- )
-
- if @project.save!
- @project.reload
-
- if @project.import_failed?
- @project.import_retry
- else
- @project.import_start
- end
- end
-
- @project
+ ).execute
end
end
end
diff --git a/lib/gitlab/gitlab_import/client.rb b/lib/gitlab/gitlab_import/client.rb
index f48ede9d067..9c00896c913 100644
--- a/lib/gitlab/gitlab_import/client.rb
+++ b/lib/gitlab/gitlab_import/client.rb
@@ -28,6 +28,10 @@ module Gitlab
client.auth_code.get_token(code, redirect_uri: redirect_uri).token
end
+ def user
+ api.get("/api/v3/user").parsed
+ end
+
def issues(project_identifier)
lazy_page_iterator(PER_PAGE) do |page|
api.get("/api/v3/projects/#{project_identifier}/issues?per_page=#{PER_PAGE}&page=#{page}").parsed
diff --git a/lib/gitlab/gitlab_import/project_creator.rb b/lib/gitlab/gitlab_import/project_creator.rb
index 6424d56f8f1..f0d7141bf56 100644
--- a/lib/gitlab/gitlab_import/project_creator.rb
+++ b/lib/gitlab/gitlab_import/project_creator.rb
@@ -10,29 +10,16 @@ module Gitlab
end
def execute
- @project = Project.new(
+ ::Projects::CreateService.new(current_user,
name: repo["name"],
path: repo["path"],
description: repo["description"],
- namespace: namespace,
- creator: current_user,
+ namespace_id: namespace.id,
visibility_level: repo["visibility_level"],
import_type: "gitlab",
import_source: repo["path_with_namespace"],
import_url: repo["http_url_to_repo"].sub("://", "://oauth2:#{current_user.gitlab_access_token}@")
- )
-
- if @project.save!
- @project.reload
-
- if @project.import_failed?
- @project.import_retry
- else
- @project.import_start
- end
- end
-
- @project
+ ).execute
end
end
end
diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb
index 5043f6a2ebd..8cdc3d4afae 100644
--- a/lib/gitlab/gitorious_import/client.rb
+++ b/lib/gitlab/gitorious_import/client.rb
@@ -27,37 +27,5 @@ module Gitlab
repo_list.to_s.split(',').map(&:strip).reject(&:blank?)
end
end
-
- Repository = Struct.new(:full_name) do
- def id
- Digest::SHA1.hexdigest(full_name)
- end
-
- def namespace
- segments.first
- end
-
- def path
- segments.last
- end
-
- def name
- path.titleize
- end
-
- def description
- ""
- end
-
- def import_url
- "#{GITORIOUS_HOST}/#{full_name}.git"
- end
-
- private
-
- def segments
- full_name.split('/')
- end
- end
end
end
diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb
index 3cbebe53997..cc9a91c91f4 100644
--- a/lib/gitlab/gitorious_import/project_creator.rb
+++ b/lib/gitlab/gitorious_import/project_creator.rb
@@ -10,29 +10,16 @@ module Gitlab
end
def execute
- @project = Project.new(
+ ::Projects::CreateService.new(current_user,
name: repo.name,
path: repo.path,
description: repo.description,
- namespace: namespace,
- creator: current_user,
+ namespace_id: namespace.id,
visibility_level: Gitlab::VisibilityLevel::PUBLIC,
import_type: "gitorious",
import_source: repo.full_name,
import_url: repo.import_url
- )
-
- if @project.save!
- @project.reload
-
- if @project.import_failed?
- @project.import_retry
- else
- @project.import_start
- end
- end
-
- @project
+ ).execute
end
end
end
diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb
new file mode 100644
index 00000000000..f702797dc6e
--- /dev/null
+++ b/lib/gitlab/gitorious_import/repository.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module GitoriousImport
+ GITORIOUS_HOST = "https://gitorious.org"
+
+ Repository = Struct.new(:full_name) do
+ def id
+ Digest::SHA1.hexdigest(full_name)
+ end
+
+ def namespace
+ segments.first
+ end
+
+ def path
+ segments.last
+ end
+
+ def name
+ path.titleize
+ end
+
+ def description
+ ""
+ end
+
+ def import_url
+ "#{GITORIOUS_HOST}/#{full_name}.git"
+ end
+
+ private
+
+ def segments
+ full_name.split('/')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/google_code_import/client.rb b/lib/gitlab/google_code_import/client.rb
new file mode 100644
index 00000000000..02f31e45f88
--- /dev/null
+++ b/lib/gitlab/google_code_import/client.rb
@@ -0,0 +1,48 @@
+module Gitlab
+ module GoogleCodeImport
+ class Client
+ attr_reader :raw_data
+
+ def self.mask_email(author)
+ parts = author.split("@", 2)
+ parts[0] = "#{parts[0][0...-3]}..."
+ parts.join("@")
+ end
+
+ def initialize(raw_data)
+ @raw_data = raw_data
+ end
+
+ def valid?
+ raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#user" && raw_data.has_key?("projects")
+ end
+
+ def repos
+ @repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.select(&:git?)
+ end
+
+ def repo(id)
+ repos.find { |repo| repo.id == id }
+ end
+
+ def user_map
+ user_map = Hash.new { |hash, user| hash[user] = self.class.mask_email(user) }
+
+ repos.each do |repo|
+ next unless repo.valid? && repo.issues
+
+ repo.issues.each do |raw_issue|
+ # Touching is enough to add the entry and masked email.
+ user_map[raw_issue["author"]["name"]]
+
+ raw_issue["comments"]["items"].each do |raw_comment|
+ user_map[raw_comment["author"]["name"]]
+ end
+ end
+ end
+
+ Hash[user_map.sort]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
new file mode 100644
index 00000000000..b5e82563ff1
--- /dev/null
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -0,0 +1,360 @@
+module Gitlab
+ module GoogleCodeImport
+ class Importer
+ attr_reader :project, :repo
+
+ def initialize(project)
+ @project = project
+
+ import_data = project.import_data.try(:data)
+ repo_data = import_data["repo"] if import_data
+ @repo = GoogleCodeImport::Repository.new(repo_data)
+
+ @closed_statuses = []
+ @known_labels = Set.new
+ end
+
+ def execute
+ return true unless repo.valid?
+
+ import_status_labels
+
+ import_labels
+
+ import_issues
+
+ true
+ end
+
+ private
+
+ def user_map
+ @user_map ||= begin
+ user_map = Hash.new { |hash, user| Client.mask_email(user) }
+
+ import_data = project.import_data.try(:data)
+ stored_user_map = import_data["user_map"] if import_data
+ user_map.update(stored_user_map) if stored_user_map
+
+ user_map
+ end
+ end
+
+ def import_status_labels
+ repo.raw_data["issuesConfig"]["statuses"].each do |status|
+ closed = !status["meansOpen"]
+ @closed_statuses << status["status"] if closed
+
+ name = nice_status_name(status["status"])
+ create_label(name)
+ @known_labels << name
+ end
+ end
+
+ def import_labels
+ repo.raw_data["issuesConfig"]["labels"].each do |label|
+ name = nice_label_name(label["label"])
+ create_label(name)
+ @known_labels << name
+ end
+ end
+
+ def import_issues
+ return unless repo.issues
+
+ while raw_issue = repo.issues.shift
+ author = user_map[raw_issue["author"]["name"]]
+ date = DateTime.parse(raw_issue["published"]).to_formatted_s(:long)
+
+ comments = raw_issue["comments"]["items"]
+ issue_comment = comments.shift
+
+ content = format_content(issue_comment["content"])
+ attachments = format_attachments(raw_issue["id"], 0, issue_comment["attachments"])
+
+ body = format_issue_body(author, date, content, attachments)
+
+ labels = []
+ raw_issue["labels"].each do |label|
+ name = nice_label_name(label)
+ labels << name
+
+ unless @known_labels.include?(name)
+ create_label(name)
+ @known_labels << name
+ end
+ end
+ labels << nice_status_name(raw_issue["status"])
+
+ assignee_id = nil
+ if raw_issue.has_key?("owner")
+ username = user_map[raw_issue["owner"]["name"]]
+
+ if username.start_with?("@")
+ username = username[1..-1]
+
+ if user = User.find_by(username: username)
+ assignee_id = user.id
+ end
+ end
+ end
+
+ issue = Issue.create!(
+ project_id: project.id,
+ title: raw_issue["title"],
+ description: body,
+ author_id: project.creator_id,
+ assignee_id: assignee_id,
+ state: raw_issue["state"] == "closed" ? "closed" : "opened"
+ )
+ issue.add_labels_by_names(labels)
+
+ if issue.iid != raw_issue["id"]
+ issue.update_attribute(:iid, raw_issue["id"])
+ end
+
+ import_issue_comments(issue, comments)
+ end
+ end
+
+ def import_issue_comments(issue, comments)
+ Note.transaction do
+ while raw_comment = comments.shift
+ next if raw_comment.has_key?("deletedBy")
+
+ content = format_content(raw_comment["content"])
+ updates = format_updates(raw_comment["updates"])
+ attachments = format_attachments(issue.iid, raw_comment["id"], raw_comment["attachments"])
+
+ next if content.blank? && updates.blank? && attachments.blank?
+
+ author = user_map[raw_comment["author"]["name"]]
+ date = DateTime.parse(raw_comment["published"]).to_formatted_s(:long)
+
+ body = format_issue_comment_body(
+ raw_comment["id"],
+ author,
+ date,
+ content,
+ updates,
+ attachments
+ )
+
+ # Needs to match order of `comment_columns` below.
+ Note.create!(
+ project_id: project.id,
+ noteable_type: "Issue",
+ noteable_id: issue.id,
+ author_id: project.creator_id,
+ note: body
+ )
+ end
+ end
+ end
+
+ def nice_label_color(name)
+ case name
+ when /\AComponent:/
+ "#fff39e"
+ when /\AOpSys:/
+ "#e2e2e2"
+ when /\AMilestone:/
+ "#fee3ff"
+
+ when *@closed_statuses.map { |s| nice_status_name(s) }
+ "#cfcfcf"
+ when "Status: New"
+ "#428bca"
+ when "Status: Accepted"
+ "#5cb85c"
+ when "Status: Started"
+ "#8e44ad"
+
+ when "Priority: Critical"
+ "#ffcfcf"
+ when "Priority: High"
+ "#deffcf"
+ when "Priority: Medium"
+ "#fff5cc"
+ when "Priority: Low"
+ "#cfe9ff"
+
+ when "Type: Defect"
+ "#d9534f"
+ when "Type: Enhancement"
+ "#44ad8e"
+ when "Type: Task"
+ "#4b6dd0"
+ when "Type: Review"
+ "#8e44ad"
+ when "Type: Other"
+ "#7f8c8d"
+ else
+ "#e2e2e2"
+ end
+ end
+
+ def nice_label_name(name)
+ name.sub("-", ": ")
+ end
+
+ def nice_status_name(name)
+ "Status: #{name}"
+ end
+
+ def linkify_issues(s)
+ s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ end
+
+ def escape_for_markdown(s)
+ s = s.gsub("*", "\\*")
+ s = s.gsub("#", "\\#")
+ s = s.gsub("`", "\\`")
+ s = s.gsub(":", "\\:")
+ s = s.gsub("-", "\\-")
+ s = s.gsub("+", "\\+")
+ s = s.gsub("_", "\\_")
+ s = s.gsub("(", "\\(")
+ s = s.gsub(")", "\\)")
+ s = s.gsub("[", "\\[")
+ s = s.gsub("]", "\\]")
+ s = s.gsub("<", "\\<")
+ s = s.gsub(">", "\\>")
+ s = s.gsub("\r", "")
+ s = s.gsub("\n", " \n")
+ s
+ end
+
+ def create_label(name)
+ color = nice_label_color(name)
+ Label.create!(project_id: project.id, name: name, color: color)
+ end
+
+ def format_content(raw_content)
+ linkify_issues(escape_for_markdown(raw_content))
+ end
+
+ def format_updates(raw_updates)
+ updates = []
+
+ if raw_updates.has_key?("status")
+ updates << "*Status: #{raw_updates["status"]}*"
+ end
+
+ if raw_updates.has_key?("owner")
+ updates << "*Owner: #{user_map[raw_updates["owner"]]}*"
+ end
+
+ if raw_updates.has_key?("cc")
+ cc = raw_updates["cc"].map do |l|
+ deleted = l.start_with?("-")
+ l = l[1..-1] if deleted
+ l = user_map[l]
+ l = "~~#{l}~~" if deleted
+ l
+ end
+
+ updates << "*Cc: #{cc.join(", ")}*"
+ end
+
+ if raw_updates.has_key?("labels")
+ labels = raw_updates["labels"].map do |l|
+ deleted = l.start_with?("-")
+ l = l[1..-1] if deleted
+ l = nice_label_name(l)
+ l = "~~#{l}~~" if deleted
+ l
+ end
+
+ updates << "*Labels: #{labels.join(", ")}*"
+ end
+
+ if raw_updates.has_key?("mergedInto")
+ updates << "*Merged into: ##{raw_updates["mergedInto"]}*"
+ end
+
+ if raw_updates.has_key?("blockedOn")
+ blocked_ons = raw_updates["blockedOn"].map do |raw_blocked_on|
+ name, id = raw_blocked_on.split(":", 2)
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ end
+ updates << "*Blocked on: #{blocked_ons.join(", ")}*"
+ end
+
+ if raw_updates.has_key?("blocking")
+ blockings = raw_updates["blocking"].map do |raw_blocked_on|
+ name, id = raw_blocked_on.split(":", 2)
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ end
+ updates << "*Blocking: #{blockings.join(", ")}*"
+ end
+
+ updates
+ end
+
+ def format_attachments(issue_id, comment_id, raw_attachments)
+ return [] unless raw_attachments
+
+ raw_attachments.map do |attachment|
+ next if attachment["isDeleted"]
+
+ filename = attachment["fileName"]
+ link = "https://storage.googleapis.com/google-code-attachments/#{@repo.name}/issue-#{issue_id}/comment-#{comment_id}/#{filename}"
+
+ text = "[#{filename}](#{link})"
+ text = "!#{text}" if filename =~ /\.(png|jpg|jpeg|gif|bmp|tiff)\z/
+ text
+ end.compact
+ end
+
+ def format_issue_comment_body(id, author, date, content, updates, attachments)
+ body = []
+ body << "*Comment #{id} by #{author} on #{date}*"
+ body << "---"
+
+ if content.blank?
+ content = "*(No comment has been entered for this change)*"
+ end
+ body << content
+
+ if updates.any?
+ body << "---"
+ body += updates
+ end
+
+ if attachments.any?
+ body << "---"
+ body += attachments
+ end
+
+ body.join("\n\n")
+ end
+
+ def format_issue_body(author, date, content, attachments)
+ body = []
+ body << "*By #{author} on #{date}*"
+ body << "---"
+
+ if content.blank?
+ content = "*(No description has been entered for this issue)*"
+ end
+ body << content
+
+ if attachments.any?
+ body << "---"
+ body += attachments
+ end
+
+ body.join("\n\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
new file mode 100644
index 00000000000..0cfeaf9d61c
--- /dev/null
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module GoogleCodeImport
+ class ProjectCreator
+ attr_reader :repo, :namespace, :current_user, :user_map
+
+ def initialize(repo, namespace, current_user, user_map = nil)
+ @repo = repo
+ @namespace = namespace
+ @current_user = current_user
+ @user_map = user_map
+ end
+
+ def execute
+ project = ::Projects::CreateService.new(current_user,
+ name: repo.name,
+ path: repo.name,
+ description: repo.summary,
+ namespace: namespace,
+ creator: current_user,
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ import_type: "google_code",
+ import_source: repo.name,
+ import_url: repo.import_url
+ ).execute
+
+ import_data = project.create_import_data(
+ data: {
+ "repo" => repo.raw_data,
+ "user_map" => user_map
+ }
+ )
+
+ project
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/google_code_import/repository.rb b/lib/gitlab/google_code_import/repository.rb
new file mode 100644
index 00000000000..ad33fc2cad2
--- /dev/null
+++ b/lib/gitlab/google_code_import/repository.rb
@@ -0,0 +1,43 @@
+module Gitlab
+ module GoogleCodeImport
+ class Repository
+ attr_accessor :raw_data
+
+ def initialize(raw_data)
+ @raw_data = raw_data
+ end
+
+ def valid?
+ raw_data.is_a?(Hash) && raw_data["kind"] == "projecthosting#project"
+ end
+
+ def id
+ raw_data["externalId"]
+ end
+
+ def name
+ raw_data["name"]
+ end
+
+ def summary
+ raw_data["summary"]
+ end
+
+ def description
+ raw_data["description"]
+ end
+
+ def git?
+ raw_data["versionControlSystem"] == "git"
+ end
+
+ def import_url
+ raw_data["repositoryUrls"].first
+ end
+
+ def issues
+ raw_data["issues"] && raw_data["issues"]["items"]
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/key_fingerprint.rb b/lib/gitlab/key_fingerprint.rb
new file mode 100644
index 00000000000..baf52ff750d
--- /dev/null
+++ b/lib/gitlab/key_fingerprint.rb
@@ -0,0 +1,55 @@
+module Gitlab
+ class KeyFingerprint
+ include Gitlab::Popen
+
+ attr_accessor :key
+
+ def initialize(key)
+ @key = key
+ end
+
+ def fingerprint
+ cmd_status = 0
+ cmd_output = ''
+
+ Tempfile.open('gitlab_key_file') do |file|
+ file.puts key
+ file.rewind
+
+ cmd = []
+ cmd.push *%W(ssh-keygen)
+ cmd.push *%W(-E md5) if explicit_fingerprint_algorithm?
+ cmd.push *%W(-lf #{file.path})
+
+ cmd_output, cmd_status = popen(cmd, '/tmp')
+ end
+
+ return nil unless cmd_status.zero?
+
+ # 16 hex bytes separated by ':', optionally starting with "MD5:"
+ fingerprint_matches = cmd_output.match(/(MD5:)?(?<fingerprint>(\h{2}:){15}\h{2})/)
+ return nil unless fingerprint_matches
+
+ fingerprint_matches[:fingerprint]
+ end
+
+ private
+
+ def explicit_fingerprint_algorithm?
+ # OpenSSH 6.8 introduces a new default output format for fingerprints.
+ # Check the version and decide which command to use.
+
+ version_output, version_status = popen(%W(ssh -V))
+ return false unless version_status.zero?
+
+ version_matches = version_output.match(/OpenSSH_(?<major>\d+)\.(?<minor>\d+)/)
+ return false unless version_matches
+
+ version_info = Gitlab::VersionInfo.new(version_matches[:major].to_i, version_matches[:minor].to_i)
+
+ required_version_info = Gitlab::VersionInfo.new(6, 8)
+
+ version_info >= required_version_info
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index 0cb24d0ccc1..d2ffa2e1fe8 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -27,8 +27,6 @@ module Gitlab
def initialize(provider)
if self.class.valid_provider?(provider)
@provider = provider
- elsif provider == 'ldap'
- @provider = self.class.providers.first
else
self.class.invalid_provider(provider)
end
@@ -82,6 +80,10 @@ module Gitlab
options['active_directory']
end
+ def block_auto_created_users
+ options['block_auto_created_users']
+ end
+
protected
def base_config
Gitlab.config.ldap
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index cfa8692659d..f7f3ba9ad7d 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -1,4 +1,4 @@
-require 'gitlab/oauth/user'
+require 'gitlab/o_auth/user'
# LDAP extension for User model
#
@@ -13,7 +13,7 @@ module Gitlab
def find_by_uid_and_provider(uid, provider)
# LDAP distinguished name is case-insensitive
identity = ::Identity.
- where(provider: [provider, :ldap]).
+ where(provider: provider).
where('lower(extern_uid) = ?', uid.downcase).last
identity && identity.user
end
@@ -39,6 +39,9 @@ module Gitlab
end
def update_user_attributes
+ return unless persisted?
+
+ gl_user.skip_reconfirmation!
gl_user.email = auth_hash.email
# Build new identity only if we dont have have same one
@@ -52,13 +55,17 @@ module Gitlab
gl_user.changed? || gl_user.identities.any?(&:changed?)
end
- def needs_blocking?
- false
+ def block_after_signup?
+ ldap_config.block_auto_created_users
end
def allowed?
Gitlab::LDAP::Access.allowed?(gl_user)
end
+
+ def ldap_config
+ Gitlab::LDAP::Config.new(auth_hash.provider)
+ end
end
end
end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index b10e85e8a8c..47c456d8dc7 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -32,12 +32,12 @@ module Gitlab
module Markdown
include IssuesHelper
- attr_reader :html_options
+ attr_reader :options, :html_options
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
- # project - extra options for the reference links as given to link_to
+ # project - the project
# html_options - extra options for the reference links as given to link_to
def gfm(text, project = @project, html_options = {})
gfm_with_options(text, {}, project, html_options)
@@ -46,9 +46,10 @@ module Gitlab
# Public: Parse the provided text with GitLab-Flavored Markdown
#
# text - the source text
- # options - parse_tasks: true - render tasks
- # - xhtml: true - output XHTML instead of HTML
- # project - extra options for the reference links as given to link_to
+ # options - parse_tasks - render tasks
+ # - xhtml - output XHTML instead of HTML
+ # - reference_only_path - Use relative path for reference links
+ # project - the project
# html_options - extra options for the reference links as given to link_to
def gfm_with_options(text, options = {}, project = @project, html_options = {})
return text if text.nil?
@@ -58,6 +59,13 @@ module Gitlab
# for gsub calls to work as we need them to.
text = text.dup.to_str
+ options.reverse_merge!(
+ parse_tasks: false,
+ xhtml: false,
+ reference_only_path: true
+ )
+
+ @options = options
@html_options = html_options
# TODO: add popups with additional information
@@ -98,12 +106,13 @@ module Gitlab
markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
result = markdown_pipeline.call(text, markdown_context)
- saveoptions = 0
+
+ save_options = 0
if options[:xhtml]
- saveoptions |= Nokogiri::XML::Node::SaveOptions::AS_XHTML
+ save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML
end
- text = result[:output].to_html(save_with: saveoptions)
+ text = result[:output].to_html(save_with: save_options)
# Extract pre blocks so they are not altered
# from http://github.github.com/github-flavored-markdown/
@@ -152,7 +161,7 @@ module Gitlab
text
end
- NAME_STR = '[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*'
+ NAME_STR = Gitlab::Regex::NAMESPACE_REGEX_STR
PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
REFERENCE_PATTERN = %r{
@@ -183,6 +192,7 @@ module Gitlab
project_path = $LAST_MATCH_INFO[:project]
if project_path
actual_project = ::Project.find_with_namespace(project_path)
+ actual_project = nil unless can?(current_user, :read_project, actual_project)
project_prefix = project_path
end
@@ -229,33 +239,38 @@ module Gitlab
end
def reference_user(identifier, project = @project, _ = nil)
- options = html_options.merge(
+ link_options = html_options.merge(
class: "gfm gfm-project_member #{html_options[:class]}"
)
if identifier == "all"
- link_to("@all", namespace_project_url(project.namespace, project), options)
+ link_to(
+ "@all",
+ namespace_project_url(project.namespace, project, only_path: options[:reference_only_path]),
+ link_options
+ )
elsif namespace = Namespace.find_by(path: identifier)
url =
- if namespace.type == "Group"
- group_url(identifier)
- else
- user_url(identifier)
+ if namespace.is_a?(Group)
+ return nil unless can?(current_user, :read_group, namespace)
+ group_url(identifier, only_path: options[:reference_only_path])
+ else
+ user_url(identifier, only_path: options[:reference_only_path])
end
-
- link_to("@#{identifier}", url, options)
+
+ link_to("@#{identifier}", url, link_options)
end
end
def reference_label(identifier, project = @project, _ = nil)
if label = project.labels.find_by(id: identifier)
- options = html_options.merge(
+ link_options = html_options.merge(
class: "gfm gfm-label #{html_options[:class]}"
)
link_to(
render_colored_label(label),
namespace_project_issues_path(project.namespace, project, label_name: label.name),
- options
+ link_options
)
end
end
@@ -263,14 +278,14 @@ module Gitlab
def reference_issue(identifier, project = @project, prefix_text = nil)
if project.default_issues_tracker?
if project.issue_exists? identifier
- url = url_for_issue(identifier, project)
+ url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
title = title_for_issue(identifier, project)
- options = html_options.merge(
+ link_options = html_options.merge(
title: "Issue: #{title}",
class: "gfm gfm-issue #{html_options[:class]}"
)
- link_to("#{prefix_text}##{identifier}", url, options)
+ link_to("#{prefix_text}##{identifier}", url, link_options)
end
else
if project.external_issue_tracker.present?
@@ -280,44 +295,46 @@ module Gitlab
end
end
- def reference_merge_request(identifier, project = @project,
- prefix_text = nil)
+ def reference_merge_request(identifier, project = @project, prefix_text = nil)
if merge_request = project.merge_requests.find_by(iid: identifier)
- options = html_options.merge(
+ link_options = html_options.merge(
title: "Merge Request: #{merge_request.title}",
class: "gfm gfm-merge_request #{html_options[:class]}"
)
url = namespace_project_merge_request_url(project.namespace, project,
- merge_request)
- link_to("#{prefix_text}!#{identifier}", url, options)
+ merge_request,
+ only_path: options[:reference_only_path])
+ link_to("#{prefix_text}!#{identifier}", url, link_options)
end
end
def reference_snippet(identifier, project = @project, _ = nil)
if snippet = project.snippets.find_by(id: identifier)
- options = html_options.merge(
+ link_options = html_options.merge(
title: "Snippet: #{snippet.title}",
class: "gfm gfm-snippet #{html_options[:class]}"
)
link_to(
"$#{identifier}",
- namespace_project_snippet_url(project.namespace, project, snippet),
- options
+ namespace_project_snippet_url(project.namespace, project, snippet,
+ only_path: options[:reference_only_path]),
+ link_options
)
end
end
def reference_commit(identifier, project = @project, prefix_text = nil)
if project.valid_repo? && commit = project.repository.commit(identifier)
- options = html_options.merge(
+ link_options = html_options.merge(
title: commit.link_title,
class: "gfm gfm-commit #{html_options[:class]}"
)
prefix_text = "#{prefix_text}@" if prefix_text
link_to(
"#{prefix_text}#{identifier}",
- namespace_project_commit_url(project.namespace, project, commit),
- options
+ namespace_project_commit_url( project.namespace, project, commit,
+ only_path: options[:reference_only_path]),
+ link_options
)
end
end
@@ -328,11 +345,11 @@ module Gitlab
inclusive = identifier !~ /\.{3}/
from_id << "^" if inclusive
- if project.valid_repo? &&
- from = project.repository.commit(from_id) &&
+ if project.valid_repo? &&
+ from = project.repository.commit(from_id) &&
to = project.repository.commit(to_id)
- options = html_options.merge(
+ link_options = html_options.merge(
title: "Commits #{from_id} through #{to_id}",
class: "gfm gfm-commit_range #{html_options[:class]}"
)
@@ -340,22 +357,23 @@ module Gitlab
link_to(
"#{prefix_text}#{identifier}",
- namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id),
- options
+ namespace_project_compare_url(project.namespace, project,
+ from: from_id, to: to_id,
+ only_path: options[:reference_only_path]),
+ link_options
)
end
end
- def reference_external_issue(identifier, project = @project,
- prefix_text = nil)
- url = url_for_issue(identifier, project)
+ def reference_external_issue(identifier, project = @project, prefix_text = nil)
+ url = url_for_issue(identifier, project, only_path: options[:reference_only_path])
title = project.external_issue_tracker.title
- options = html_options.merge(
+ link_options = html_options.merge(
title: "Issue in #{title}",
class: "gfm gfm-issue #{html_options[:class]}"
)
- link_to("#{prefix_text}##{identifier}", url, options)
+ link_to("#{prefix_text}##{identifier}", url, link_options)
end
# Turn list items that start with "[ ]" into HTML checkbox inputs.
diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index ce52beec78e..ce52beec78e 100644
--- a/lib/gitlab/oauth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
diff --git a/lib/gitlab/oauth/user.rb b/lib/gitlab/o_auth/user.rb
index c023d275703..2f5c217d764 100644
--- a/lib/gitlab/oauth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -86,7 +86,7 @@ module Gitlab
def user_attributes
{
name: auth_hash.name,
- username: ::User.clean_username(auth_hash.username),
+ username: ::Namespace.clean_path(auth_hash.username),
email: auth_hash.email,
password: auth_hash.password,
password_confirmation: auth_hash.password,
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 1058d4c43d9..a502a8fe9cd 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,94 +1,94 @@
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
- attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits, :commit_ranges
+ attr_accessor :project, :current_user, :references
- include Markdown
+ include ::Gitlab::Markdown
- def initialize
- @users, @labels, @issues, @merge_requests, @snippets, @commits, @commit_ranges =
- [], [], [], [], [], [], []
+ def initialize(project, current_user = nil)
+ @project = project
+ @current_user = current_user
end
- def analyze(string, project)
- text = string.dup
+ def can?(user, action, subject)
+ Ability.abilities.allowed?(user, action, subject)
+ end
+
+ def analyze(text)
+ text = text.dup
# Remove preformatted/code blocks so that references are not included
text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| '' }
text.gsub!(%r{^```.*?^```}m) { |match| '' }
- parse_references(text, project)
+ @references = Hash.new { |hash, type| hash[type] = [] }
+ parse_references(text)
end
# Given a valid project, resolve the extracted identifiers of the requested type to
# model objects.
- def users_for(project)
- users.map do |entry|
- project.users.where(username: entry[:id]).first
- end.reject(&:nil?)
+ def users
+ references[:user].uniq.map do |project, identifier|
+ if identifier == "all"
+ project.team.members.flatten
+ elsif namespace = Namespace.find_by(path: identifier)
+ if namespace.is_a?(Group)
+ namespace.users
+ else
+ namespace.owner
+ end
+ end
+ end.flatten.compact.uniq
end
- def labels_for(project = nil)
- labels.map do |entry|
- project.labels.where(id: entry[:id]).first
- end.reject(&:nil?)
+ def labels
+ references[:label].uniq.map do |project, identifier|
+ project.labels.where(id: identifier).first
+ end.compact.uniq
end
- def issues_for(project = nil)
- issues.map do |entry|
- if should_lookup?(project, entry[:project])
- entry[:project].issues.where(iid: entry[:id]).first
+ def issues
+ references[:issue].uniq.map do |project, identifier|
+ if project.default_issues_tracker?
+ project.issues.where(iid: identifier).first
end
- end.reject(&:nil?)
+ end.compact.uniq
end
- def merge_requests_for(project = nil)
- merge_requests.map do |entry|
- if should_lookup?(project, entry[:project])
- entry[:project].merge_requests.where(iid: entry[:id]).first
- end
- end.reject(&:nil?)
+ def merge_requests
+ references[:merge_request].uniq.map do |project, identifier|
+ project.merge_requests.where(iid: identifier).first
+ end.compact.uniq
end
- def snippets_for(project)
- snippets.map do |entry|
- project.snippets.where(id: entry[:id]).first
- end.reject(&:nil?)
+ def snippets
+ references[:snippet].uniq.map do |project, identifier|
+ project.snippets.where(id: identifier).first
+ end.compact.uniq
end
- def commits_for(project = nil)
- commits.map do |entry|
- repo = entry[:project].repository if entry[:project]
- if should_lookup?(project, entry[:project])
- repo.commit(entry[:id]) if repo
- end
- end.reject(&:nil?)
+ def commits
+ references[:commit].uniq.map do |project, identifier|
+ repo = project.repository
+ repo.commit(identifier) if repo
+ end.compact.uniq
end
- def commit_ranges_for(project = nil)
- commit_ranges.map do |entry|
- repo = entry[:project].repository if entry[:project]
- if repo && should_lookup?(project, entry[:project])
- from_id, to_id = entry[:id].split(/\.{2,3}/, 2)
+ def commit_ranges
+ references[:commit_range].uniq.map do |project, identifier|
+ repo = project.repository
+ if repo
+ from_id, to_id = identifier.split(/\.{2,3}/, 2)
[repo.commit(from_id), repo.commit(to_id)]
end
- end.reject(&:nil?)
+ end.compact.uniq
end
private
def reference_link(type, identifier, project, _)
- # Append identifier to the appropriate collection.
- send("#{type}s") << { project: project, id: identifier }
- end
-
- def should_lookup?(project, entry_project)
- if entry_project.nil?
- false
- else
- project.nil? || entry_project.default_issues_tracker?
- end
+ references[type] << [project, identifier]
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index cf6e260f257..0571574aa4f 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,49 +2,66 @@ module Gitlab
module Regex
extend self
- def username_regex
- default_regex
+ NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze
+
+ def namespace_regex
+ @namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
+ end
+
+ def namespace_regex_message
+ "can contain only letters, digits, '_', '-' and '.'. " \
+ "Cannot start with '-' or end in '.'." \
+ end
+
+
+ def namespace_name_regex
+ @namespace_name_regex ||= /\A[a-zA-Z0-9_\-\. ]*\z/.freeze
end
- def username_regex_message
- default_regex_message
+ def namespace_name_regex_message
+ "can contain only letters, digits, '_', '-', '.' and space."
end
+
def project_name_regex
- /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/
+ @project_name_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/.freeze
end
- def project_regex_message
- "can contain only letters, digits, '_', '-' and '.' and space. " \
+ def project_name_regex_message
+ "can contain only letters, digits, '_', '-', '.' and space. " \
"It must start with letter, digit or '_'."
end
- def name_regex
- /\A[a-zA-Z0-9_\-\. ]*\z/
+
+ def project_path_regex
+ @project_path_regex ||= /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/.freeze
end
- def name_regex_message
- "can contain only letters, digits, '_', '-' and '.' and space."
+ def project_path_regex_message
+ "can contain only letters, digits, '_', '-' and '.'. " \
+ "Cannot start with '-' or end in '.git'" \
end
- def path_regex
- default_regex
+
+ def file_name_regex
+ @file_name_regex ||= /\A[a-zA-Z0-9_\-\.]*\z/.freeze
end
- def path_regex_message
- default_regex_message
+ def file_name_regex_message
+ "can contain only letters, digits, '_', '-' and '.'. "
end
+
def archive_formats_regex
- #|zip|tar| tar.gz | tar.bz2 |
- /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/
+ # |zip|tar| tar.gz | tar.bz2 |
+ @archive_formats_regex ||= /(zip|tar|tar\.gz|tgz|gz|tar\.bz2|tbz|tbz2|tb2|bz2)/.freeze
end
def git_reference_regex
# Valid git ref regex, see:
# https://www.kernel.org/pub/software/scm/git/docs/git-check-ref-format.html
- %r{
+ @git_reference_regex ||= %r{
(?!
(?# doesn't begins with)
\/| (?# rule #6)
@@ -60,18 +77,7 @@ module Gitlab
(?# doesn't end with)
(?<!\.lock) (?# rule #1)
(?<![\/.]) (?# rule #6-7)
- }x
- end
-
- protected
-
- def default_regex_message
- "can contain only letters, digits, '_', '-' and '.'. " \
- "Cannot start with '-' or end in '.git'" \
- end
-
- def default_regex
- /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\.]*(?<!\.git)\z/
+ }x.freeze
end
end
end
diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb
index f24c6199c44..398643d68de 100644
--- a/lib/gitlab/satellite/satellite.rb
+++ b/lib/gitlab/satellite/satellite.rb
@@ -1,5 +1,10 @@
module Gitlab
module Satellite
+ autoload :DeleteFileAction, 'gitlab/satellite/files/delete_file_action'
+ autoload :EditFileAction, 'gitlab/satellite/files/edit_file_action'
+ autoload :FileAction, 'gitlab/satellite/files/file_action'
+ autoload :NewFileAction, 'gitlab/satellite/files/new_file_action'
+
class CheckoutFailed < StandardError; end
class CommitFailed < StandardError; end
class PushFailed < StandardError; end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 0230fbb010b..84445b3bf2f 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -27,9 +27,9 @@ namespace :gitlab do
backup = Backup::Manager.new
backup.unpack
- Rake::Task["gitlab:backup:db:restore"].invoke
- Rake::Task["gitlab:backup:repo:restore"].invoke
- Rake::Task["gitlab:backup:uploads:restore"].invoke
+ Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db")
+ Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
+ Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
Rake::Task["gitlab:shell:setup"].invoke
backup.cleanup
@@ -38,8 +38,13 @@ namespace :gitlab do
namespace :repo do
task create: :environment do
$progress.puts "Dumping repositories ...".blue
- Backup::Repository.new.dump
- $progress.puts "done".green
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("repositories")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Repository.new.dump
+ $progress.puts "done".green
+ end
end
task restore: :environment do
@@ -52,8 +57,13 @@ namespace :gitlab do
namespace :db do
task create: :environment do
$progress.puts "Dumping database ... ".blue
- Backup::Database.new.dump
- $progress.puts "done".green
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("db")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Database.new.dump
+ $progress.puts "done".green
+ end
end
task restore: :environment do
@@ -66,8 +76,13 @@ namespace :gitlab do
namespace :uploads do
task create: :environment do
$progress.puts "Dumping uploads ... ".blue
- Backup::Uploads.new.dump
- $progress.puts "done".green
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("uploads")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Uploads.new.dump
+ $progress.puts "done".green
+ end
end
task restore: :environment do
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index d791b7155f9..04a2eb12db0 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -687,6 +687,23 @@ namespace :gitlab do
end
end
+ namespace :repo do
+ desc "GITLAB | Check the integrity of the repositories managed by GitLab"
+ task check: :environment do
+ namespace_dirs = Dir.glob(
+ File.join(Gitlab.config.gitlab_shell.repos_path, '*')
+ )
+
+ namespace_dirs.each do |namespace_dir|
+ repo_dirs = Dir.glob(File.join(namespace_dir, '*'))
+ repo_dirs.each do |dir|
+ puts "\nChecking repo at #{dir}"
+ system(*%w(git fsck), chdir: dir)
+ end
+ end
+ end
+ end
+
# Helper methods
##########################
diff --git a/lib/tasks/gitlab/test.rake b/lib/tasks/gitlab/test.rake
index b4076f8238f..b4c0ae3ff79 100644
--- a/lib/tasks/gitlab/test.rake
+++ b/lib/tasks/gitlab/test.rake
@@ -2,6 +2,7 @@ namespace :gitlab do
desc "GITLAB | Run all tests"
task :test do
cmds = [
+ %W(rake brakeman),
%W(rake rubocop),
%W(rake spinach),
%W(rake spec),
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
new file mode 100644
index 00000000000..a0909cec3bd
--- /dev/null
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe AutocompleteController do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user) }
+ let!(:user2) { create(:user) }
+
+ context 'project members' do
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+
+ get(:users, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it { body.should be_kind_of(Array) }
+ it { body.size.should eq(1) }
+ it { body.first["username"].should == user.username }
+ end
+
+ context 'group members' do
+ let(:group) { create(:group) }
+
+ before do
+ sign_in(user)
+ group.add_owner(user)
+
+ get(:users, group_id: group.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it { body.should be_kind_of(Array) }
+ it { body.size.should eq(1) }
+ it { body.first["username"].should == user.username }
+ end
+
+ context 'all users' do
+ before do
+ sign_in(user)
+ get(:users)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it { body.should be_kind_of(Array) }
+ it { body.size.should eq(User.count) }
+ end
+end
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
new file mode 100644
index 00000000000..93535ced7ae
--- /dev/null
+++ b/spec/controllers/help_controller_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe HelpController do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET #show' do
+ context 'for Markdown formats' do
+ context 'when requested file exists' do
+ before do
+ get :show, category: 'ssh', file: 'README', format: :md
+ end
+
+ it 'assigns to @markdown' do
+ expect(assigns[:markdown]).not_to be_empty
+ end
+
+ it 'renders HTML' do
+ expect(response).to render_template('show.html.haml')
+ expect(response.content_type).to eq 'text/html'
+ end
+ end
+
+ context 'when requested file is missing' do
+ it 'renders not found' do
+ get :show, category: 'foo', file: 'bar', format: :md
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ context 'for image formats' do
+ context 'when requested file exists' do
+ it 'renders the raw file' do
+ get :show, category: 'workflow/protected_branches',
+ file: 'protected_branches1', format: :png
+ expect(response).to be_success
+ expect(response.content_type).to eq 'image/png'
+ expect(response.headers['Content-Disposition']).to match(/^inline;/)
+ end
+ end
+
+ context 'when requested file is missing' do
+ it 'renders not found' do
+ get :show, category: 'foo', file: 'bar', format: :png
+ expect(response).to be_not_found
+ end
+ end
+ end
+
+ context 'for other formats' do
+ it 'always renders not found' do
+ get :show, category: 'ssh', file: 'README', format: :foo
+ expect(response).to be_not_found
+ end
+ end
+ end
+end
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 5dd4124061c..c31563e6d77 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -55,24 +55,109 @@ describe Import::BitbucketController do
end
describe "POST create" do
- before do
- @repo = {
- slug: 'vim',
- owner: "john"
+ let(:bitbucket_username) { user.username }
+
+ let(:bitbucket_user) {
+ {
+ user: {
+ username: bitbucket_username
+ }
}.with_indifferent_access
- end
+ }
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "john", owner: user)
- expect(Gitlab::BitbucketImport::KeyAdder).
- to receive(:new).with(@repo, user).
- and_return(double(execute: true))
- expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
+ let(:bitbucket_repo) {
+ {
+ slug: "vim",
+ owner: bitbucket_username
+ }.with_indifferent_access
+ }
+
+ before do
+ allow(Gitlab::BitbucketImport::KeyAdder).
+ to receive(:new).with(bitbucket_repo, user).
and_return(double(execute: true))
- controller.stub_chain(:client, :project).and_return(@repo)
- post :create, format: :js
+ controller.stub_chain(:client, :user).and_return(bitbucket_user)
+ controller.stub_chain(:client, :project).and_return(bitbucket_repo)
+ end
+
+ context "when the repository owner is the Bitbucket user" do
+ context "when the Bitbucket user and GitLab user's usernames match" do
+ it "takes the current user's namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, user.namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the Bitbucket user and GitLab user's usernames don't match" do
+ let(:bitbucket_username) { "someone_else" }
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, user.namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when the repository owner is not the Bitbucket user" do
+ let(:other_username) { "someone_else" }
+
+ before do
+ bitbucket_repo["owner"] = other_username
+ end
+
+ context "when a namespace with the Bitbucket user's username already exists" do
+ let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+
+ context "when the namespace is owned by the GitLab user" do
+ it "takes the existing namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, existing_namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the namespace is not owned by the GitLab user" do
+ before do
+ existing_namespace.owner = create(:user)
+ existing_namespace.save
+ end
+
+ it "doesn't create a project" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ not_to receive(:new)
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when a namespace with the Bitbucket user's username doesn't exist" do
+ it "creates the namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ post :create, format: :js
+
+ expect(Namespace.where(name: other_username).first).not_to be_nil
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, an_instance_of(Group), user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 5b967bfcc0c..3d3846b2e3a 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -56,18 +56,98 @@ describe Import::GithubController do
end
describe "POST create" do
+ let(:github_username) { user.username }
+
+ let(:github_user) {
+ OpenStruct.new(login: github_username)
+ }
+
+ let(:github_repo) {
+ OpenStruct.new(name: 'vim', full_name: "#{github_username}/vim", owner: OpenStruct.new(login: github_username))
+ }
+
before do
- @repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim', owner: OpenStruct.new(login: "john"))
+ controller.stub_chain(:client, :user).and_return(github_user)
+ controller.stub_chain(:client, :repo).and_return(github_repo)
+ end
+
+ context "when the repository owner is the GitHub user" do
+ context "when the GitHub user and GitLab user's usernames match" do
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, user.namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the GitHub user and GitLab user's usernames don't match" do
+ let(:github_username) { "someone_else" }
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, user.namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
end
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "john", owner: user)
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
- and_return(double(execute: true))
- controller.stub_chain(:client, :repo).and_return(@repo)
+ context "when the repository owner is not the GitHub user" do
+ let(:other_username) { "someone_else" }
+
+ before do
+ github_repo.owner = OpenStruct.new(login: other_username)
+ end
+
+ context "when a namespace with the GitHub user's username already exists" do
+ let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+
+ context "when the namespace is owned by the GitLab user" do
+ it "takes the existing namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, existing_namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the namespace is not owned by the GitLab user" do
+ before do
+ existing_namespace.owner = create(:user)
+ existing_namespace.save
+ end
+
+ it "doesn't create a project" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ not_to receive(:new)
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when a namespace with the GitHub user's username doesn't exist" do
+ it "creates the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ post :create, format: :js
+
+ expect(Namespace.where(name: other_username).first).not_to be_nil
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, an_instance_of(Group), user).
+ and_return(double(execute: true))
- post :create, format: :js
+ post :create, format: :js
+ end
+ end
end
end
end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index b6b86b1bcee..112e51d431e 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -48,23 +48,105 @@ describe Import::GitlabController do
end
describe "POST create" do
- before do
- @repo = {
+ let(:gitlab_username) { user.username }
+
+ let(:gitlab_user) {
+ {
+ username: gitlab_username
+ }.with_indifferent_access
+ }
+
+ let(:gitlab_repo) {
+ {
path: 'vim',
- path_with_namespace: 'asd/vim',
- owner: {name: "john"},
- namespace: {path: "john"}
+ path_with_namespace: "#{gitlab_username}/vim",
+ owner: { name: gitlab_username },
+ namespace: { path: gitlab_username }
}.with_indifferent_access
+ }
+
+ before do
+ controller.stub_chain(:client, :user).and_return(gitlab_user)
+ controller.stub_chain(:client, :project).and_return(gitlab_repo)
+ end
+
+ context "when the repository owner is the GitLab.com user" do
+ context "when the GitLab.com user and GitLab server user's usernames match" do
+ it "takes the current user's namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, user.namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the GitLab.com user and GitLab server user's usernames don't match" do
+ let(:gitlab_username) { "someone_else" }
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, user.namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
end
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "john", owner: user)
- expect(Gitlab::GitlabImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
- and_return(double(execute: true))
- controller.stub_chain(:client, :project).and_return(@repo)
+ context "when the repository owner is not the GitLab.com user" do
+ let(:other_username) { "someone_else" }
+
+ before do
+ gitlab_repo["namespace"]["path"] = other_username
+ end
+
+ context "when a namespace with the GitLab.com user's username already exists" do
+ let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+
+ context "when the namespace is owned by the GitLab server user" do
+ it "takes the existing namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, existing_namespace, user).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
+ end
+
+ context "when the namespace is not owned by the GitLab server user" do
+ before do
+ existing_namespace.owner = create(:user)
+ existing_namespace.save
+ end
+
+ it "doesn't create a project" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ not_to receive(:new)
+
+ post :create, format: :js
+ end
+ end
+ end
+
+ context "when a namespace with the GitLab.com user's username doesn't exist" do
+ it "creates the namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ post :create, format: :js
+
+ expect(Namespace.where(name: other_username).first).not_to be_nil
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, an_instance_of(Group), user).
+ and_return(double(execute: true))
- post :create, format: :js
+ post :create, format: :js
+ end
+ end
end
end
end
diff --git a/spec/controllers/import/google_code_controller_spec.rb b/spec/controllers/import/google_code_controller_spec.rb
new file mode 100644
index 00000000000..037cddb4600
--- /dev/null
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Import::GoogleCodeController do
+ let(:user) { create(:user) }
+ let(:dump_file) { fixture_file_upload(Rails.root + 'spec/fixtures/GoogleCodeProjectHosting.json', 'application/json') }
+
+ before do
+ sign_in(user)
+ end
+
+ describe "POST callback" do
+ it "stores Google Takeout dump list in session" do
+ post :callback, dump_file: dump_file
+
+ expect(session[:google_code_dump]).to be_a(Hash)
+ expect(session[:google_code_dump]["kind"]).to eq("projecthosting#user")
+ expect(session[:google_code_dump]).to have_key("projects")
+ end
+ end
+
+ describe "GET status" do
+ before do
+ @repo = OpenStruct.new(name: 'vim')
+ controller.stub_chain(:client, :valid?).and_return(true)
+ end
+
+ it "assigns variables" do
+ @project = create(:project, import_type: 'google_code', creator_id: user.id)
+ controller.stub_chain(:client, :repos).and_return([@repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([@repo])
+ end
+
+ it "does not show already added project" do
+ @project = create(:project, import_type: 'google_code', creator_id: user.id, import_source: 'vim')
+ controller.stub_chain(:client, :repos).and_return([@repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([])
+ end
+ end
+end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
new file mode 100644
index 00000000000..9c8619722cd
--- /dev/null
+++ b/spec/controllers/namespaces_controller_spec.rb
@@ -0,0 +1,121 @@
+require 'spec_helper'
+
+describe NamespacesController do
+ let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+
+ describe "GET show" do
+ context "when the namespace belongs to a user" do
+ let!(:other_user) { create(:user) }
+
+ it "redirects to the user's page" do
+ get :show, id: other_user.username
+
+ expect(response).to redirect_to(user_path(other_user))
+ end
+ end
+
+ context "when the namespace belongs to a group" do
+ let!(:group) { create(:group) }
+ let!(:project) { create(:project, namespace: group) }
+
+ context "when the group has public projects" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ context "when not signed in" do
+ it "redirects to the group's page" do
+ get :show, id: group.path
+
+ expect(response).to redirect_to(group_path(group))
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ it "redirects to the group's page" do
+ get :show, id: group.path
+
+ expect(response).to redirect_to(group_path(group))
+ end
+ end
+ end
+
+ context "when the project doesn't have public projects" do
+ context "when not signed in" do
+ it "redirects to the sign in page" do
+ get :show, id: group.path
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ context "when the user has access to the project" do
+ before do
+ project.team << [user, :master]
+ end
+
+ context "when the user is blocked" do
+ before do
+ user.block
+ project.team << [user, :master]
+ end
+
+ it "redirects to the sign in page" do
+ get :show, id: group.path
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "redirects to the group's page" do
+ get :show, id: group.path
+
+ expect(response).to redirect_to(group_path(group))
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ it "responds with status 404" do
+ get :show, id: group.path
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+
+ context "when the namespace doesn't exist" do
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ it "responds with status 404" do
+ get :show, id: "doesntexist"
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context "when not signed in" do
+ it "redirects to the sign in page" do
+ get :show, id: "doesntexist"
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/refs_controller_spec.rb b/spec/controllers/projects/refs_controller_spec.rb
new file mode 100644
index 00000000000..c254ab7cb6e
--- /dev/null
+++ b/spec/controllers/projects/refs_controller_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Projects::RefsController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ describe 'GET #logs_tree' do
+ def default_get(format = :html)
+ get :logs_tree, namespace_id: project.namespace.to_param,
+ project_id: project.to_param, id: 'master',
+ path: 'foo/bar/baz.html', format: format
+ end
+
+ def xhr_get(format = :html)
+ xhr :get, :logs_tree, namespace_id: project.namespace.to_param,
+ project_id: project.to_param, id: 'master',
+ path: 'foo/bar/baz.html', format: format
+ end
+
+ it 'never throws MissingTemplate' do
+ expect { default_get }.not_to raise_error
+ expect { xhr_get }.not_to raise_error
+ end
+
+ it 'renders 404 for non-JS requests' do
+ xhr_get
+
+ expect(response).to be_not_found
+ end
+
+ it 'renders JS' do
+ xhr_get(:js)
+ expect(response).to be_success
+ end
+ end
+end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
new file mode 100644
index 00000000000..91856ed0cc0
--- /dev/null
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -0,0 +1,65 @@
+require "spec_helper"
+
+describe Projects::RepositoriesController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ describe "GET archive" do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+
+ allow(ArchiveRepositoryService).to receive(:new).and_return(service)
+ end
+
+ let(:service) { ArchiveRepositoryService.new(project, "master", "zip") }
+
+ it "executes ArchiveRepositoryService" do
+ expect(ArchiveRepositoryService).to receive(:new).with(project, "master", "zip")
+ expect(service).to receive(:execute)
+
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+ end
+
+ context "when the service raises an error" do
+
+ before do
+ allow(service).to receive(:execute).and_raise("Archive failed")
+ end
+
+ it "renders Not Found" do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context "when the service doesn't return a path" do
+
+ before do
+ allow(service).to receive(:execute).and_return(nil)
+ end
+
+ it "reloads the page" do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+
+ expect(response).to redirect_to(archive_namespace_project_repository_path(project.namespace, project, ref: "master", format: "zip"))
+ end
+ end
+
+ context "when the service returns a path" do
+
+ let(:path) { Rails.root.join("spec/fixtures/dk.png").to_s }
+
+ before do
+ allow(service).to receive(:execute).and_return(path)
+ end
+
+ it "sends the file" do
+ get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip"
+
+ expect(response.body).to eq(File.binread(path))
+ end
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index fc103e5b133..a5c335c82bc 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -101,21 +101,12 @@ FactoryGirl.define do
user
end
- factory :key_with_a_space_in_the_middle do
- key do
- "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa ++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0="
- end
- end
-
factory :another_key do
key do
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ"
end
- end
- factory :invalid_key do
- key do
- "ssh-rsa this_is_invalid_key=="
+ factory :another_deploy_key, class: 'DeployKey' do
end
end
end
diff --git a/spec/factories_spec.rb b/spec/factories_spec.rb
index c8e218d4d03..457859dedaf 100644
--- a/spec/factories_spec.rb
+++ b/spec/factories_spec.rb
@@ -1,12 +1,6 @@
require 'spec_helper'
-INVALID_FACTORIES = [
- :key_with_a_space_in_the_middle,
- :invalid_key,
-]
-
FactoryGirl.factories.map(&:name).each do |factory_name|
- next if INVALID_FACTORIES.include?(factory_name)
describe "#{factory_name} factory" do
it 'should be valid' do
expect(build(factory_name)).to be_valid
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index 41088ce8271..8c6b669ce78 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -6,7 +6,7 @@ describe 'Help Pages', feature: true do
login_as :user
end
it 'replace the variable $your_email with the email of the user' do
- visit help_page_path(category: 'ssh', file: 'README.md')
+ visit help_page_path('ssh', 'README')
expect(page).to have_content("ssh-keygen -t rsa -C \"#{@user.email}\"")
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index a2db57ad908..e5f33d5a25a 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -95,7 +95,7 @@ describe 'Issues', feature: true do
let(:issue) { @issue }
it 'should allow filtering by issues with no specified milestone' do
- visit namespace_project_issues_path(project.namespace, project, milestone_id: '0')
+ visit namespace_project_issues_path(project.namespace, project, milestone_id: IssuableFinder::NONE)
expect(page).not_to have_content 'foobar'
expect(page).to have_content 'barbaz'
@@ -111,7 +111,7 @@ describe 'Issues', feature: true do
end
it 'should allow filtering by issues with no specified assignee' do
- visit namespace_project_issues_path(project.namespace, project, assignee_id: '0')
+ visit namespace_project_issues_path(project.namespace, project, assignee_id: IssuableFinder::NONE)
expect(page).to have_content 'foobar'
expect(page).not_to have_content 'barbaz'
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 21a3a4bf937..4cfaab03caf 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,14 +1,37 @@
require 'spec_helper'
-describe 'Users', feature: true do
- describe "GET /users/sign_in" do
- it "should create a new user account" do
- visit new_user_session_path
- fill_in "user_name", with: "Name Surname"
- fill_in "user_username", with: "Great"
- fill_in "user_email", with: "name@mail.com"
- fill_in "user_password_sign_up", with: "password1234"
- expect { click_button "Sign up" }.to change { User.count }.by(1)
- end
+feature 'Users' do
+ around do |ex|
+ old_url_options = Rails.application.routes.default_url_options
+ Rails.application.routes.default_url_options = { host: 'example.foo' }
+ ex.run
+ Rails.application.routes.default_url_options = old_url_options
+ end
+
+ scenario 'GET /users/sign_in creates a new user account' do
+ visit new_user_session_path
+ fill_in 'user_name', with: 'Name Surname'
+ fill_in 'user_username', with: 'Great'
+ fill_in 'user_email', with: 'name@mail.com'
+ fill_in 'user_password_sign_up', with: 'password1234'
+ expect { click_button 'Sign up' }.to change { User.count }.by(1)
+ end
+
+ scenario 'Successful user signin invalidates password reset token' do
+ user = create(:user)
+ expect(user.reset_password_token).to be_nil
+
+ visit new_user_password_path
+ fill_in 'user_email', with: user.email
+ click_button 'Reset password'
+
+ user.reload
+ expect(user.reset_password_token).not_to be_nil
+
+ login_with(user)
+ expect(current_path).to eq root_path
+
+ user.reload
+ expect(user.reset_password_token).to be_nil
end
end
diff --git a/spec/fixtures/GoogleCodeProjectHosting.json b/spec/fixtures/GoogleCodeProjectHosting.json
new file mode 100644
index 00000000000..d05e77271ae
--- /dev/null
+++ b/spec/fixtures/GoogleCodeProjectHosting.json
@@ -0,0 +1,407 @@
+{
+ "kind" : "projecthosting#user",
+ "id" : "@WRRVSlFXARlCVgB6",
+ "projects" : [ {
+ "kind" : "projecthosting#project",
+ "name" : "pmn",
+ "externalId" : "pmn",
+ "htmlLink" : "/p/pmn/",
+ "summary" : "Shows an icon in the system tray when you have new emails",
+ "description" : "IMAP client that shows an icon in the system tray when you have new emails.",
+ "labels" : [ "Mail" ],
+ "versionControlSystem" : "svn",
+ "repositoryUrls" : [ "https://pmn.googlecode.com/svn/" ],
+ "issuesConfig" : {
+ "kind" : "projecthosting#projectIssueConfig",
+ "statuses" : [ {
+ "status" : "New",
+ "meansOpen" : true,
+ "description" : "Issue has not had initial review yet"
+ }, {
+ "status" : "Accepted",
+ "meansOpen" : true,
+ "description" : "Problem reproduced / Need acknowledged"
+ }, {
+ "status" : "Started",
+ "meansOpen" : true,
+ "description" : "Work on this issue has begun"
+ }, {
+ "status" : "Fixed",
+ "meansOpen" : false,
+ "description" : "Developer made source code changes, QA should verify"
+ }, {
+ "status" : "Verified",
+ "meansOpen" : false,
+ "description" : "QA has verified that the fix worked"
+ }, {
+ "status" : "Invalid",
+ "meansOpen" : false,
+ "description" : "This was not a valid issue report"
+ }, {
+ "status" : "Duplicate",
+ "meansOpen" : false,
+ "description" : "This report duplicates an existing issue"
+ }, {
+ "status" : "WontFix",
+ "meansOpen" : false,
+ "description" : "We decided to not take action on this issue"
+ }, {
+ "status" : "Done",
+ "meansOpen" : false,
+ "description" : "The requested non-coding task was completed"
+ } ],
+ "labels" : [ {
+ "label" : "Type-Defect",
+ "description" : "Report of a software defect"
+ }, {
+ "label" : "Type-Enhancement",
+ "description" : "Request for enhancement"
+ }, {
+ "label" : "Type-Task",
+ "description" : "Work item that doesn't change the code or docs"
+ }, {
+ "label" : "Type-Review",
+ "description" : "Request for a source code review"
+ }, {
+ "label" : "Type-Other",
+ "description" : "Some other kind of issue"
+ }, {
+ "label" : "Priority-Critical",
+ "description" : "Must resolve in the specified milestone"
+ }, {
+ "label" : "Priority-High",
+ "description" : "Strongly want to resolve in the specified milestone"
+ }, {
+ "label" : "Priority-Medium",
+ "description" : "Normal priority"
+ }, {
+ "label" : "Priority-Low",
+ "description" : "Might slip to later milestone"
+ }, {
+ "label" : "OpSys-All",
+ "description" : "Affects all operating systems"
+ }, {
+ "label" : "OpSys-Windows",
+ "description" : "Affects Windows users"
+ }, {
+ "label" : "OpSys-Linux",
+ "description" : "Affects Linux users"
+ }, {
+ "label" : "OpSys-OSX",
+ "description" : "Affects Mac OS X users"
+ }, {
+ "label" : "Milestone-Release1.0",
+ "description" : "All essential functionality working"
+ }, {
+ "label" : "Component-UI",
+ "description" : "Issue relates to program UI"
+ }, {
+ "label" : "Component-Logic",
+ "description" : "Issue relates to application logic"
+ }, {
+ "label" : "Component-Persistence",
+ "description" : "Issue relates to data storage components"
+ }, {
+ "label" : "Component-Scripts",
+ "description" : "Utility and installation scripts"
+ }, {
+ "label" : "Component-Docs",
+ "description" : "Issue relates to end-user documentation"
+ }, {
+ "label" : "Security",
+ "description" : "Security risk to users"
+ }, {
+ "label" : "Performance",
+ "description" : "Performance issue"
+ }, {
+ "label" : "Usability",
+ "description" : "Affects program usability"
+ }, {
+ "label" : "Maintainability",
+ "description" : "Hinders future changes"
+ } ],
+ "prompts" : [ {
+ "name" : "Defect report from user",
+ "title" : "Enter one-line summary",
+ "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nPlease provide any additional information below.\n",
+ "titleMustBeEdited" : true,
+ "status" : "New",
+ "labels" : [ "Type-Defect", "Priority-Medium" ]
+ }, {
+ "name" : "Defect report from developer",
+ "title" : "Enter one-line summary",
+ "description" : "What steps will reproduce the problem?\n1. \n2. \n3. \n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.\n",
+ "titleMustBeEdited" : true,
+ "status" : "Accepted",
+ "labels" : [ "Type-Defect", "Priority-Medium" ],
+ "membersOnly" : true
+ }, {
+ "name" : "Review request",
+ "title" : "Code review request",
+ "description" : "Branch name:\n\nPurpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk\n",
+ "status" : "New",
+ "labels" : [ "Type-Review", "Priority-Medium" ],
+ "membersOnly" : true,
+ "defaultToMember" : false
+ } ],
+ "defaultPromptForMembers" : 1,
+ "defaultPromptForNonMembers" : 0
+ },
+ "role" : "owner",
+ "members" : [ {
+ "kind" : "projecthosting#issuePerson",
+ "name" : "mrovi9000",
+ "htmlLink" : "https://code.google.com/u/106736353629303906862/"
+ } ],
+ "issues" : {
+ "kind" : "projecthosting#issueList",
+ "totalResults" : 0,
+ "items" : [ ]
+ }
+ }, {
+ "kind" : "projecthosting#project",
+ "name" : "tint2",
+ "externalId" : "tint2",
+ "htmlLink" : "/p/tint2/",
+ "summary" : "tint2 is a lightweight panel/taskbar.",
+ "description" : "tint2 is a simple _*panel/taskbar*_ unintrusive and light (memory / cpu / aestetic). <br>We follow freedesktop specifications.\r\n \r\n=== 0.11 features ===\r\n * panel with taskbar, systray, clock and battery status\r\n * easy to customize : color/transparency on font, icon, border and background\r\n * pager like capability : send task from one workspace to another, switch workspace\r\n * multi-monitor capability : one panel per monitor, show task from current monitor\r\n * customize mouse event\r\n * window manager's menu\r\n * tooltip\r\n * autohide\r\n * clock timezones\r\n * real & fake transparency with autodetection of composite manager\r\n * panel's theme switcher 'tint2conf' \r\n\r\n=== Other project ===\r\n * Lightweight volume control http://softwarebakery.com/maato/volumeicon.html\r\n * Lightweight calendar http://code.google.com/p/gsimplecal/\r\n * Graphical config tool http://code.google.com/p/tintwizard/\r\n * Command line theme switcher http://github.com/dbbolton/scripts/blob/master/tint2theme\r\n\r\n\r\n=== Snapshot SVN ===\r\n\r\nhttp://img252.imageshack.us/img252/1433/wallpaper2td.jpg\r\n\r\n\r\n",
+ "labels" : [ "taskbar", "panel", "lightweight", "desktop", "openbox", "pager", "tint2" ],
+ "versionControlSystem" : "git",
+ "repositoryUrls" : [ "https://tint2.googlecode.com/git/" ],
+ "issuesConfig" : {
+ "kind" : "projecthosting#projectIssueConfig",
+ "defaultColumns" : [ "ID", "Status", "Type", "Milestone", "Priority", "Component", "Owner", "Summary", "Modified", "Stars" ],
+ "defaultSorting" : [ "-ID" ],
+ "statuses" : [ {
+ "status" : "New",
+ "meansOpen" : true,
+ "description" : "Issue has not had initial review yet"
+ }, {
+ "status" : "NeedInfo",
+ "meansOpen" : true,
+ "description" : "More information is needed before deciding what action should be taken"
+ }, {
+ "status" : "Accepted",
+ "meansOpen" : true,
+ "description" : "A Defect that a developer has reproduced or an Enhancement that a developer has committed to addressing"
+ }, {
+ "status" : "Wishlist",
+ "meansOpen" : true,
+ "description" : "An Enhancement which is valid, but no developers have committed to addressing"
+ }, {
+ "status" : "Started",
+ "meansOpen" : true,
+ "description" : "Work on this issue has begun"
+ }, {
+ "status" : "Fixed",
+ "meansOpen" : false,
+ "description" : "Work has completed"
+ }, {
+ "status" : "Invalid",
+ "meansOpen" : false,
+ "description" : "This was not a valid issue report"
+ }, {
+ "status" : "Duplicate",
+ "meansOpen" : false,
+ "description" : "This report duplicates an existing issue"
+ }, {
+ "status" : "WontFix",
+ "meansOpen" : false,
+ "description" : "We decided to not take action on this issue"
+ }, {
+ "status" : "Incomplete",
+ "meansOpen" : false,
+ "description" : "Not enough information and no activity for a long period of time"
+ } ],
+ "labels" : [ {
+ "label" : "Type-Defect",
+ "description" : "Report of a software defect"
+ }, {
+ "label" : "Type-Enhancement",
+ "description" : "Request for enhancement"
+ }, {
+ "label" : "Type-Task",
+ "description" : "Work item that does not change the code"
+ }, {
+ "label" : "Type-Review",
+ "description" : "Request for a source code review"
+ }, {
+ "label" : "Type-Other",
+ "description" : "Some other kind of issue"
+ }, {
+ "label" : "Milestone-0.12",
+ "description" : "Fix should be included in release 0.12"
+ }, {
+ "label" : "Priority-Critical",
+ "description" : "Must resolve in the specified milestone"
+ }, {
+ "label" : "Priority-High",
+ "description" : "Strongly want to resolve in the specified milestone"
+ }, {
+ "label" : "Priority-Medium",
+ "description" : "Normal priority"
+ }, {
+ "label" : "Priority-Low",
+ "description" : "Might slip to later milestone"
+ }, {
+ "label" : "OpSys-All",
+ "description" : "Affects all operating systems"
+ }, {
+ "label" : "OpSys-Windows",
+ "description" : "Affects Windows users"
+ }, {
+ "label" : "OpSys-Linux",
+ "description" : "Affects Linux users"
+ }, {
+ "label" : "OpSys-OSX",
+ "description" : "Affects Mac OS X users"
+ }, {
+ "label" : "Security",
+ "description" : "Security risk to users"
+ }, {
+ "label" : "Performance",
+ "description" : "Performance issue"
+ }, {
+ "label" : "Usability",
+ "description" : "Affects program usability"
+ }, {
+ "label" : "Maintainability",
+ "description" : "Hinders future changes"
+ }, {
+ "label" : "Component-Panel",
+ "description" : "Issue relates to the panel (e.g. positioning, hiding, transparency)"
+ }, {
+ "label" : "Component-Taskbar",
+ "description" : "Issue relates to the taskbar (e.g. tasks, multiple desktops)"
+ }, {
+ "label" : "Component-Battery",
+ "description" : "Issue relates to the battery"
+ }, {
+ "label" : "Component-Systray",
+ "description" : "Issue relates to the system tray"
+ }, {
+ "label" : "Component-Clock",
+ "description" : "Issue relates to the clock"
+ }, {
+ "label" : "Component-Launcher",
+ "description" : "Issue relates to the launcher"
+ }, {
+ "label" : "Component-Tint2conf",
+ "description" : "Issue relates to the configuration GUI (tint2conf)"
+ }, {
+ "label" : "Component-Docs",
+ "description" : "Issue relates to end-user documentation"
+ }, {
+ "label" : "Component-New",
+ "description" : "Issue describes a new component proposal"
+ } ],
+ "prompts" : [ {
+ "name" : "Defect report from user",
+ "title" : "Enter one-line summary",
+ "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nWhat version of the product are you using? On what operating system?\n\n\nWhich window manager (e.g. openbox, xfwm, metacity, mutter, kwin) or\nwhich desktop environment (e.g. Gnome 2, Gnome 3, LXDE, XFCE, KDE)\nare you using?\n\n\nPlease provide any additional information below. It might be helpful\nto attach your tint2rc file (usually located at ~/.config/tint2/tint2rc).",
+ "titleMustBeEdited" : true,
+ "status" : "New",
+ "labels" : [ "Priority-Medium" ],
+ "defaultToMember" : true
+ }, {
+ "name" : "Defect report from developer",
+ "title" : "Enter one-line summary",
+ "description" : "What steps will reproduce the problem?\n1.\n2.\n3.\n\nWhat is the expected output? What do you see instead?\n\n\nPlease use labels and text to provide additional information.",
+ "titleMustBeEdited" : true,
+ "status" : "Accepted",
+ "labels" : [ "Type-Defect", "Priority-Medium" ],
+ "membersOnly" : true,
+ "defaultToMember" : true
+ }, {
+ "name" : "Review request",
+ "title" : "Code review request",
+ "description" : "Purpose of code changes on this branch:\n\n\nWhen reviewing my code changes, please focus on:\n\n\nAfter the review, I'll merge this branch into:\n/trunk",
+ "status" : "New",
+ "labels" : [ "Type-Review", "Priority-Medium" ],
+ "membersOnly" : true,
+ "defaultToMember" : true
+ } ],
+ "defaultPromptForMembers" : 1,
+ "defaultPromptForNonMembers" : 0,
+ "usersCanSetLabels" : false
+ },
+ "role" : "owner",
+ "issues" : {
+ "kind" : "projecthosting#issueList",
+ "totalResults" : 473,
+ "items" : [ {
+ "kind" : "projecthosting#issue",
+ "id" : 169,
+ "title" : "Scrolling through tasks",
+ "summary" : "Scrolling through tasks",
+ "stars" : 1,
+ "starred" : false,
+ "status" : "Fixed",
+ "state" : "closed",
+ "labels" : [ "Type-Enhancement", "Priority-Medium" ],
+ "author" : {
+ "kind" : "projecthosting#issuePerson",
+ "name" : "schattenpr...",
+ "htmlLink" : "https://code.google.com/u/106498139506637530000/"
+ },
+ "owner" : {
+ "kind" : "projecthosting#issuePerson",
+ "name" : "thilo...",
+ "htmlLink" : "https://code.google.com/u/104224918623172014000/"
+ },
+ "updated" : "2009-11-18T05:14:58.000Z",
+ "published" : "2009-11-18T00:20:19.000Z",
+ "closed" : "2009-11-18T05:14:58.000Z",
+ "projectId" : "tint2",
+ "canComment" : true,
+ "canEdit" : true,
+ "comments" : {
+ "kind" : "projecthosting#issueCommentList",
+ "totalResults" : 2,
+ "items" : [ {
+ "id" : 0,
+ "kind" : "projecthosting#issueComment",
+ "author" : {
+ "kind" : "projecthosting#issuePerson",
+ "name" : "schattenpr...",
+ "htmlLink" : "https://code.google.com/u/10649813950663753000/"
+ },
+ "content" : "I like to scroll through the tasks with my scrollwheel (like in fluxbox). \r\n\r\nPatch is attached that adds two new mouse-actions (next_task+prev_task) \r\nthat can be used for exactly that purpose. \r\n\r\nall the best!",
+ "published" : "2009-11-18T00:20:19.000Z",
+ "updates" : {
+ "kind" : "projecthosting#issueCommentUpdate"
+ },
+ "canDelete" : true,
+ "attachments" : [ {
+ "attachmentId" : "8901002890399325565",
+ "fileName" : "tint2_task_scrolling.diff",
+ "fileSize" : 3059,
+ "mimetype" : "text/x-c++; charset=us-ascii"
+ }, {
+ "attachmentId" : "000",
+ "fileName" : "screenshot.png",
+ "fileSize" : 0,
+ "mimetype" : "image/png"
+ } ]
+ }, {
+ "id" : 1,
+ "kind" : "projecthosting#issueComment",
+ "author" : {
+ "kind" : "projecthosting#issuePerson",
+ "name" : "thilo...",
+ "htmlLink" : "https://code.google.com/u/104224918623172014000/"
+ },
+ "content" : "applied, thanks.\r\n",
+ "published" : "2009-11-18T05:14:58.000Z",
+ "updates" : {
+ "kind" : "projecthosting#issueCommentUpdate",
+ "status" : "Fixed",
+ "labels" : [ "-Type-Defect", "Type-Enhancement" ]
+ },
+ "canDelete" : true
+ } ]
+ }
+ } ]
+ }
+ } ]
+}
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 4c11709ed6e..015a66f7fa0 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -225,25 +225,29 @@ describe ApplicationHelper do
end
describe 'link_to' do
-
it 'should not include rel=nofollow for internal links' do
- expect(link_to('Home', root_path)).to eq("<a href=\"/\">Home</a>")
+ expect(link_to('Home', root_path)).to eq('<a href="/">Home</a>')
end
it 'should include rel=nofollow for external links' do
- expect(link_to('Example', 'http://www.example.com')).to eq("<a href=\"http://www.example.com\" rel=\"nofollow\">Example</a>")
+ expect(link_to('Example', 'http://www.example.com')).
+ to eq '<a href="http://www.example.com" rel="nofollow">Example</a>'
+ end
+
+ it 'should include rel=nofollow for external links and honor existing html_options' do
+ expect(link_to('Example', 'http://www.example.com', class: 'toggle', data: {toggle: 'dropdown'}))
+ .to eq '<a class="toggle" data-toggle="dropdown" href="http://www.example.com" rel="nofollow">Example</a>'
end
- it 'should include re=nofollow for external links and honor existing html_options' do
- expect(
- link_to('Example', 'http://www.example.com', class: 'toggle', data: {toggle: 'dropdown'})
- ).to eq("<a class=\"toggle\" data-toggle=\"dropdown\" href=\"http://www.example.com\" rel=\"nofollow\">Example</a>")
+ it 'should include rel=nofollow for external links and preserve other rel values' do
+ expect(link_to('Example', 'http://www.example.com', rel: 'noreferrer'))
+ .to eq '<a href="http://www.example.com" rel="noreferrer nofollow">Example</a>'
end
- it 'should include rel=nofollow for external links and preserver other rel values' do
- expect(
- link_to('Example', 'http://www.example.com', rel: 'noreferrer')
- ).to eq("<a href=\"http://www.example.com\" rel=\"noreferrer nofollow\">Example</a>")
+ it 'should not include rel=nofollow for external links on the same host as GitLab' do
+ expect(Gitlab.config.gitlab).to receive(:host).and_return('example.foo')
+ expect(link_to('Example', 'http://example.foo/bar')).
+ to eq '<a href="http://example.foo/bar">Example</a>'
end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index c631acc591d..944e743675c 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -4,6 +4,11 @@ describe GitlabMarkdownHelper do
include ApplicationHelper
include IssuesHelper
+ # TODO: Properly test this
+ def can?(*)
+ true
+ end
+
let!(:project) { create(:project) }
let(:empty_project) { create(:empty_project) }
@@ -15,6 +20,9 @@ describe GitlabMarkdownHelper do
let(:snippet) { create(:project_snippet, project: project) }
let(:member) { project.project_members.where(user_id: user).first }
+ # Helper expects a current_user method.
+ let(:current_user) { user }
+
def url_helper(image_name)
File.join(root_url, 'assets', image_name)
end
@@ -651,7 +659,7 @@ describe GitlabMarkdownHelper do
end
it "should leave ref-like href of 'manual' links untouched" do
- expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_url(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n")
+ expect(markdown("why not [inspect !#{merge_request.iid}](http://example.tld/#!#{merge_request.iid})")).to eq("<p>why not <a href=\"http://example.tld/#!#{merge_request.iid}\">inspect </a><a class=\"gfm gfm-merge_request \" href=\"#{namespace_project_merge_request_path(project.namespace, project, merge_request)}\" title=\"Merge Request: #{merge_request.title}\">!#{merge_request.iid}</a><a href=\"http://example.tld/#!#{merge_request.iid}\"></a></p>\n")
end
it "should leave ref-like src of images untouched" do
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
new file mode 100644
index 00000000000..c052981fe73
--- /dev/null
+++ b/spec/helpers/icons_helper_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe IconsHelper do
+ describe 'file_type_icon_class' do
+ it 'returns folder class' do
+ expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder'
+ end
+
+ it 'returns share class' do
+ expect(file_type_icon_class('file', '120000', 'link')).to eq 'share'
+ end
+
+ it 'returns file-pdf-o class with .pdf' do
+ expect(file_type_icon_class('file', 0, 'filename.pdf')).to eq 'file-pdf-o'
+ end
+
+ it 'returns file-image-o class with .jpg' do
+ expect(file_type_icon_class('file', 0, 'filename.jpg')).to eq 'file-image-o'
+ end
+
+ it 'returns file-image-o class with .JPG' do
+ expect(file_type_icon_class('file', 0, 'filename.JPG')).to eq 'file-image-o'
+ end
+
+ it 'returns file-image-o class with .png' do
+ expect(file_type_icon_class('file', 0, 'filename.png')).to eq 'file-image-o'
+ end
+
+ it 'returns file-archive-o class with .tar' do
+ expect(file_type_icon_class('file', 0, 'filename.tar')).to eq 'file-archive-o'
+ end
+
+ it 'returns file-archive-o class with .TAR' do
+ expect(file_type_icon_class('file', 0, 'filename.TAR')).to eq 'file-archive-o'
+ end
+
+ it 'returns file-archive-o class with .tar.gz' do
+ expect(file_type_icon_class('file', 0, 'filename.tar.gz')).to eq 'file-archive-o'
+ end
+
+ it 'returns file-audio-o class with .mp3' do
+ expect(file_type_icon_class('file', 0, 'filename.mp3')).to eq 'file-audio-o'
+ end
+
+ it 'returns file-audio-o class with .MP3' do
+ expect(file_type_icon_class('file', 0, 'filename.MP3')).to eq 'file-audio-o'
+ end
+
+ it 'returns file-audio-o class with .wav' do
+ expect(file_type_icon_class('file', 0, 'filename.wav')).to eq 'file-audio-o'
+ end
+
+ it 'returns file-video-o class with .avi' do
+ expect(file_type_icon_class('file', 0, 'filename.avi')).to eq 'file-video-o'
+ end
+
+ it 'returns file-video-o class with .AVI' do
+ expect(file_type_icon_class('file', 0, 'filename.AVI')).to eq 'file-video-o'
+ end
+
+ it 'returns file-video-o class with .mp4' do
+ expect(file_type_icon_class('file', 0, 'filename.mp4')).to eq 'file-video-o'
+ end
+
+ it 'returns file-word-o class with .doc' do
+ expect(file_type_icon_class('file', 0, 'filename.doc')).to eq 'file-word-o'
+ end
+
+ it 'returns file-word-o class with .DOC' do
+ expect(file_type_icon_class('file', 0, 'filename.DOC')).to eq 'file-word-o'
+ end
+
+ it 'returns file-word-o class with .docx' do
+ expect(file_type_icon_class('file', 0, 'filename.docx')).to eq 'file-word-o'
+ end
+
+ it 'returns file-excel-o class with .xls' do
+ expect(file_type_icon_class('file', 0, 'filename.xls')).to eq 'file-excel-o'
+ end
+
+ it 'returns file-excel-o class with .XLS' do
+ expect(file_type_icon_class('file', 0, 'filename.XLS')).to eq 'file-excel-o'
+ end
+
+ it 'returns file-excel-o class with .xlsx' do
+ expect(file_type_icon_class('file', 0, 'filename.xlsx')).to eq 'file-excel-o'
+ end
+
+ it 'returns file-excel-o class with .ppt' do
+ expect(file_type_icon_class('file', 0, 'filename.ppt')).to eq 'file-powerpoint-o'
+ end
+
+ it 'returns file-excel-o class with .PPT' do
+ expect(file_type_icon_class('file', 0, 'filename.PPT')).to eq 'file-powerpoint-o'
+ end
+
+ it 'returns file-excel-o class with .pptx' do
+ expect(file_type_icon_class('file', 0, 'filename.pptx')).to eq 'file-powerpoint-o'
+ end
+
+ it 'returns file-text-o class with .unknow' do
+ expect(file_type_icon_class('file', 0, 'filename.unknow')).to eq 'file-text-o'
+ end
+
+ it 'returns file-text-o class with no extension' do
+ expect(file_type_icon_class('file', 0, 'CHANGELOG')).to eq 'file-text-o'
+ end
+ end
+end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index aef1108e333..e99c3f5bc11 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe SubmoduleHelper do
+ include RepoHelpers
+
describe 'submodule links' do
let(:submodule_item) { double(id: 'hash', path: 'rack') }
let(:config) { Gitlab.config.gitlab }
@@ -111,6 +113,39 @@ describe SubmoduleHelper do
expect(submodule_links(submodule_item)).to eq([ repo.submodule_url_for, nil ])
end
end
+
+ context 'submodules with relative links' do
+ let(:group) { create(:group) }
+ let(:project) { create(:project, group: group) }
+
+ before do
+ self.instance_variable_set(:@project, project)
+ end
+
+ it 'one level down' do
+ commit_id = sample_commit[:id]
+ result = relative_self_links('../test.git', commit_id)
+ expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"])
+ end
+
+ it 'two levels down' do
+ commit_id = sample_commit[:id]
+ result = relative_self_links('../../test.git', commit_id)
+ expect(result).to eq(["/#{group.path}/test", "/#{group.path}/test/tree/#{commit_id}"])
+ end
+
+ it 'one level down with namespace and repo' do
+ commit_id = sample_commit[:id]
+ result = relative_self_links('../foobar/test.git', commit_id)
+ expect(result).to eq(["/foobar/test", "/foobar/test/tree/#{commit_id}"])
+ end
+
+ it 'two levels down with namespace and repo' do
+ commit_id = sample_commit[:id]
+ result = relative_self_links('../foobar/baz/test.git', commit_id)
+ expect(result).to eq(["/baz/test", "/baz/test/tree/#{commit_id}"])
+ end
+ end
end
def stub_url(url)
diff --git a/spec/lib/file_size_validator_spec.rb b/spec/lib/file_size_validator_spec.rb
new file mode 100644
index 00000000000..5c89c854714
--- /dev/null
+++ b/spec/lib/file_size_validator_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe 'Gitlab::FileSizeValidatorSpec' do
+ let(:validator) { FileSizeValidator.new(options) }
+ let(:attachment) { AttachmentUploader.new }
+ let(:note) { create(:note) }
+
+ describe 'options uses an integer' do
+ let(:options) { { maximum: 10, attributes: { attachment: attachment } } }
+
+ it 'attachment exceeds maximum limit' do
+ allow(attachment).to receive(:size) { 100 }
+ validator.validate_each(note, :attachment, attachment)
+ expect(note.errors).to have_key(:attachment)
+ end
+
+ it 'attachment under maximum limit' do
+ allow(attachment).to receive(:size) { 1 }
+ validator.validate_each(note, :attachment, attachment)
+ expect(note.errors).not_to have_key(:attachment)
+ end
+ end
+
+ describe 'options uses a symbol' do
+ let(:options) { { maximum: :test,
+ attributes: { attachment: attachment } } }
+ before do
+ allow(note).to receive(:test) { 10 }
+ end
+
+ it 'attachment exceeds maximum limit' do
+ allow(attachment).to receive(:size) { 100 }
+ validator.validate_each(note, :attachment, attachment)
+ expect(note.errors).to have_key(:attachment)
+ end
+
+ it 'attachment under maximum limit' do
+ allow(attachment).to receive(:size) { 1 }
+ validator.validate_each(note, :attachment, attachment)
+ expect(note.errors).not_to have_key(:attachment)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index f5523105848..0ec6a43f681 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -8,8 +8,12 @@ describe Gitlab::BitbucketImport::ProjectCreator do
is_private: true,
owner: "asd"}.with_indifferent_access
}
- let(:namespace){ create(:namespace) }
+ let(:namespace){ create(:group, owner: user) }
+ before do
+ namespace.add_owner(user)
+ end
+
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb
index c96ee78e5fd..cb7b0fbb890 100644
--- a/spec/lib/gitlab/closing_issue_extractor_spec.rb
+++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb
@@ -5,126 +5,128 @@ describe Gitlab::ClosingIssueExtractor do
let(:issue) { create(:issue, project: project) }
let(:iid1) { issue.iid }
- describe :closed_by_message_in_project do
+ subject { described_class.new(project, project.creator) }
+
+ describe "#closed_by_message" do
context 'with a single reference' do
it do
message = "Awesome commit (Closes ##{iid1})"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Awesome commit (closes ##{iid1})"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Closed ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "closed ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Closing ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "closing ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Close ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "close ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Awesome commit (Fixes ##{iid1})"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Awesome commit (fixes ##{iid1})"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Fixed ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "fixed ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Fixing ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "fixing ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Fix ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "fix ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Awesome commit (Resolves ##{iid1})"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Awesome commit (resolves ##{iid1})"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Resolved ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "resolved ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Resolving ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "resolving ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "Resolve ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
it do
message = "resolve ##{iid1}"
- expect(subject.closed_by_message_in_project(message, project)).to eq([issue])
+ expect(subject.closed_by_message(message)).to eq([issue])
end
end
@@ -137,28 +139,28 @@ describe Gitlab::ClosingIssueExtractor do
it 'fetches issues in single line message' do
message = "Closes ##{iid1} and fix ##{iid2}"
- expect(subject.closed_by_message_in_project(message, project)).
+ expect(subject.closed_by_message(message)).
to eq([issue, other_issue])
end
it 'fetches comma-separated issues references in single line message' do
message = "Closes ##{iid1}, closes ##{iid2}"
- expect(subject.closed_by_message_in_project(message, project)).
+ expect(subject.closed_by_message(message)).
to eq([issue, other_issue])
end
it 'fetches comma-separated issues numbers in single line message' do
message = "Closes ##{iid1}, ##{iid2} and ##{iid3}"
- expect(subject.closed_by_message_in_project(message, project)).
+ expect(subject.closed_by_message(message)).
to eq([issue, other_issue, third_issue])
end
it 'fetches issues in multi-line message' do
message = "Awesome commit (closes ##{iid1})\nAlso fixes ##{iid2}"
- expect(subject.closed_by_message_in_project(message, project)).
+ expect(subject.closed_by_message(message)).
to eq([issue, other_issue])
end
@@ -166,7 +168,7 @@ describe Gitlab::ClosingIssueExtractor do
message = "Awesome commit (closes ##{iid1})\n"\
"Also fixing issues ##{iid2}, ##{iid3} and #4"
- expect(subject.closed_by_message_in_project(message, project)).
+ expect(subject.closed_by_message(message)).
to eq([issue, other_issue, third_issue])
end
end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 8d594a112d4..3bf52cb685e 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -10,7 +10,11 @@ describe Gitlab::GithubImport::ProjectCreator do
clone_url: "https://gitlab.com/asd/vim.git",
owner: OpenStruct.new(login: "john"))
}
- let(:namespace){ create(:namespace) }
+ let(:namespace){ create(:group, owner: user) }
+
+ before do
+ namespace.add_owner(user)
+ end
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
diff --git a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
index 4c0d64ed138..3cefe4ea8e2 100644
--- a/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/project_creator_spec.rb
@@ -10,7 +10,11 @@ describe Gitlab::GitlabImport::ProjectCreator do
http_url_to_repo: "https://gitlab.com/asd/vim.git",
owner: {name: "john"}}.with_indifferent_access
}
- let(:namespace){ create(:namespace) }
+ let(:namespace){ create(:group, owner: user) }
+
+ before do
+ namespace.add_owner(user)
+ end
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
diff --git a/spec/lib/gitlab/gitorious_import/project_creator.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
index cf2318bb3a2..c1125ca6357 100644
--- a/spec/lib/gitlab/gitorious_import/project_creator.rb
+++ b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
@@ -3,14 +3,17 @@ require 'spec_helper'
describe Gitlab::GitoriousImport::ProjectCreator do
let(:user) { create(:user) }
let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
- let(:namespace){ create(:namespace) }
+ let(:namespace){ create(:group, owner: user) }
+
+ before do
+ namespace.add_owner(user)
+ end
it 'creates project' do
allow_any_instance_of(Project).to receive(:add_import_job)
project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user)
- project_creator.execute
- project = Project.last
+ project = project_creator.execute
expect(project.name).to eq("Bar Baz Qux")
expect(project.path).to eq("bar-baz-qux")
diff --git a/spec/lib/gitlab/google_code_import/client_spec.rb b/spec/lib/gitlab/google_code_import/client_spec.rb
new file mode 100644
index 00000000000..d2bf871daa8
--- /dev/null
+++ b/spec/lib/gitlab/google_code_import/client_spec.rb
@@ -0,0 +1,34 @@
+require "spec_helper"
+
+describe Gitlab::GoogleCodeImport::Client do
+ let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) }
+ subject { described_class.new(raw_data) }
+
+ describe "#valid?" do
+ context "when the data is valid" do
+ it "returns true" do
+ expect(subject).to be_valid
+ end
+ end
+
+ context "when the data is invalid" do
+ let(:raw_data) { "No clue" }
+
+ it "returns true" do
+ expect(subject).to_not be_valid
+ end
+ end
+ end
+
+ describe "#repos" do
+ it "returns only Git repositories" do
+ expect(subject.repos.length).to eq(1)
+ end
+ end
+
+ describe "#repo" do
+ it "returns the referenced repository" do
+ expect(subject.repo("tint2").name).to eq("tint2")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/google_code_import/importer_spec.rb b/spec/lib/gitlab/google_code_import/importer_spec.rb
new file mode 100644
index 00000000000..1c4503ae0ef
--- /dev/null
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -0,0 +1,84 @@
+require "spec_helper"
+
+describe Gitlab::GoogleCodeImport::Importer do
+ let(:mapped_user) { create(:user, username: "thilo123") }
+ let(:raw_data) { JSON.parse(File.read(Rails.root.join("spec/fixtures/GoogleCodeProjectHosting.json"))) }
+ let(:client) { Gitlab::GoogleCodeImport::Client.new(raw_data) }
+ let(:import_data) {
+ {
+ "repo" => client.repo("tint2").raw_data,
+ "user_map" => {
+ "thilo..." => "@#{mapped_user.username}"
+ }
+ }
+ }
+ let(:project) { create(:project) }
+ subject { described_class.new(project) }
+
+ before do
+ project.create_import_data(data: import_data)
+ end
+
+ describe "#execute" do
+
+ it "imports status labels" do
+ subject.execute
+
+ %w(New NeedInfo Accepted Wishlist Started Fixed Invalid Duplicate WontFix Incomplete).each do |status|
+ expect(project.labels.find_by(name: "Status: #{status}")).to_not be_nil
+ end
+ end
+
+ it "imports labels" do
+ subject.execute
+
+ %w(
+ Type-Defect Type-Enhancement Type-Task Type-Review Type-Other Milestone-0.12 Priority-Critical
+ Priority-High Priority-Medium Priority-Low OpSys-All OpSys-Windows OpSys-Linux OpSys-OSX Security
+ Performance Usability Maintainability Component-Panel Component-Taskbar Component-Battery
+ Component-Systray Component-Clock Component-Launcher Component-Tint2conf Component-Docs Component-New
+ ).each do |label|
+ label.sub!("-", ": ")
+ expect(project.labels.find_by(name: label)).to_not be_nil
+ end
+ end
+
+ it "imports issues" do
+ subject.execute
+
+ issue = project.issues.first
+ expect(issue).to_not be_nil
+ expect(issue.iid).to eq(169)
+ expect(issue.author).to eq(project.creator)
+ expect(issue.assignee).to eq(mapped_user)
+ expect(issue.state).to eq("closed")
+ expect(issue.label_names).to include("Priority: Medium")
+ expect(issue.label_names).to include("Status: Fixed")
+ expect(issue.label_names).to include("Type: Enhancement")
+ expect(issue.title).to eq("Scrolling through tasks")
+ expect(issue.state).to eq("closed")
+ expect(issue.description).to include("schattenpr...")
+ expect(issue.description).to include("November 18, 2009 00:20")
+ expect(issue.description).to include('I like to scroll through the tasks with my scrollwheel \(like in fluxbox\).')
+ expect(issue.description).to include('Patch is attached that adds two new mouse\-actions \(next\_taskprev\_task\)')
+ expect(issue.description).to include('that can be used for exactly that purpose.')
+ expect(issue.description).to include('all the best!')
+ expect(issue.description).to include('[tint2_task_scrolling.diff](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/tint2_task_scrolling.diff)')
+ expect(issue.description).to include('![screenshot.png](https://storage.googleapis.com/google-code-attachments/tint2/issue-169/comment-0/screenshot.png)')
+ end
+
+ it "imports issue comments" do
+ subject.execute
+
+ note = project.issues.first.notes.first
+ expect(note).to_not be_nil
+ expect(note.note).to include("Comment 1")
+ expect(note.note).to include("@#{mapped_user.username}")
+ expect(note.note).to include("November 18, 2009 05:14")
+ expect(note.note).to include("applied, thanks.")
+ expect(note.note).to include("Status: Fixed")
+ expect(note.note).to include("~~Type: Defect~~")
+ expect(note.note).to include("Type: Enhancement")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/google_code_import/project_creator_spec.rb b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
new file mode 100644
index 00000000000..7a224538b8b
--- /dev/null
+++ b/spec/lib/gitlab/google_code_import/project_creator_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::GoogleCodeImport::ProjectCreator do
+ let(:user) { create(:user) }
+ let(:repo) {
+ Gitlab::GoogleCodeImport::Repository.new(
+ "name" => 'vim',
+ "summary" => 'VI Improved',
+ "repositoryUrls" => [ "https://vim.googlecode.com/git/" ]
+ )
+ }
+ let(:namespace){ create(:group, owner: user) }
+
+ before do
+ namespace.add_owner(user)
+ end
+
+ it 'creates project' do
+ allow_any_instance_of(Project).to receive(:add_import_job)
+
+ project_creator = Gitlab::GoogleCodeImport::ProjectCreator.new(repo, namespace, user)
+ project = project_creator.execute
+
+ expect(project.import_url).to eq("https://vim.googlecode.com/git/")
+ expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+end
diff --git a/spec/lib/gitlab/key_fingerprint_spec.rb b/spec/lib/gitlab/key_fingerprint_spec.rb
new file mode 100644
index 00000000000..266eab6e793
--- /dev/null
+++ b/spec/lib/gitlab/key_fingerprint_spec.rb
@@ -0,0 +1,12 @@
+require "spec_helper"
+
+describe Gitlab::KeyFingerprint do
+ let(:key) { "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=" }
+ let(:fingerprint) { "3f:a2:ee:de:b5:de:53:c3:aa:2f:9c:45:24:4c:47:7b" }
+
+ describe "#fingerprint" do
+ it "generates the key's fingerprint" do
+ expect(Gitlab::KeyFingerprint.new(key).fingerprint).to eq(fingerprint)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/ldap/config_spec.rb
index 2df2beca7a6..00e9076c787 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/ldap/config_spec.rb
@@ -16,19 +16,5 @@ describe Gitlab::LDAP::Config do
it "raises an error if a unknow provider is used" do
expect{ Gitlab::LDAP::Config.new 'unknown' }.to raise_error
end
-
- context "if 'ldap' is the provider name" do
- let(:provider) { 'ldap' }
-
- context "and 'ldap' is not in defined as a provider" do
- before { Gitlab::LDAP::Config.stub(providers: %w{ldapmain}) }
-
- it "uses the first provider" do
- # Fetch the provider_name attribute from 'options' so that we know
- # that the 'options' Hash is not empty/nil.
- expect(config.options['provider_name']).to eq('ldapmain')
- end
- end
- end
end
end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 4f93545feb6..42015c28c81 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe Gitlab::LDAP::User do
- let(:gl_user) { Gitlab::LDAP::User.new(auth_hash) }
+ let(:ldap_user) { Gitlab::LDAP::User.new(auth_hash) }
+ let(:gl_user) { ldap_user.gl_user }
let(:info) do
{
name: 'John',
@@ -16,17 +17,17 @@ describe Gitlab::LDAP::User do
describe :changed? do
it "marks existing ldap user as changed" do
existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
- expect(gl_user.changed?).to be_truthy
+ expect(ldap_user.changed?).to be_truthy
end
it "marks existing non-ldap user if the email matches as changed" do
existing_user = create(:user, email: 'john@example.com')
- expect(gl_user.changed?).to be_truthy
+ expect(ldap_user.changed?).to be_truthy
end
it "dont marks existing ldap user as changed" do
existing_user = create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain')
- expect(gl_user.changed?).to be_falsey
+ expect(ldap_user.changed?).to be_falsey
end
end
@@ -34,12 +35,12 @@ describe Gitlab::LDAP::User do
it "finds the user if already existing" do
existing_user = create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain')
- expect{ gl_user.save }.to_not change{ User.count }
+ expect{ ldap_user.save }.to_not change{ User.count }
end
it "connects to existing non-ldap user if the email matches" do
existing_user = create(:omniauth_user, email: 'john@example.com', provider: "twitter")
- expect{ gl_user.save }.to_not change{ User.count }
+ expect{ ldap_user.save }.to_not change{ User.count }
existing_user.reload
expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid'
@@ -47,7 +48,59 @@ describe Gitlab::LDAP::User do
end
it "creates a new user if not found" do
- expect{ gl_user.save }.to change{ User.count }.by(1)
+ expect{ ldap_user.save }.to change{ User.count }.by(1)
+ end
+ end
+
+
+ describe 'blocking' do
+ context 'signup' do
+ context 'dont block on create' do
+ before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false }
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true }
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).to be_blocked
+ end
+ end
+ end
+
+ context 'sign-in' do
+ before do
+ ldap_user.save
+ ldap_user.gl_user.activate
+ end
+
+ context 'dont block on create' do
+ before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: false }
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
+
+ context 'block on create' do
+ before { Gitlab::LDAP::Config.any_instance.stub block_auto_created_users: true }
+
+ it do
+ ldap_user.save
+ expect(gl_user).to be_valid
+ expect(gl_user).not_to be_blocked
+ end
+ end
end
end
end
diff --git a/spec/lib/gitlab/oauth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
index 5eb77b492b2..5eb77b492b2 100644
--- a/spec/lib/gitlab/oauth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
diff --git a/spec/lib/gitlab/oauth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index 44cdd1e4fab..44cdd1e4fab 100644
--- a/spec/lib/gitlab/oauth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index b3f4bb5aeda..c9fb62b61ae 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -1,73 +1,76 @@
require 'spec_helper'
describe Gitlab::ReferenceExtractor do
+ let(:project) { create(:project) }
+ subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
+
it 'extracts username references' do
- subject.analyze('this contains a @user reference', nil)
- expect(subject.users).to eq([{ project: nil, id: 'user' }])
+ subject.analyze('this contains a @user reference')
+ expect(subject.references[:user]).to eq([[project, 'user']])
end
it 'extracts issue references' do
- subject.analyze('this one talks about issue #1234', nil)
- expect(subject.issues).to eq([{ project: nil, id: '1234' }])
+ subject.analyze('this one talks about issue #1234')
+ expect(subject.references[:issue]).to eq([[project, '1234']])
end
it 'extracts JIRA issue references' do
- subject.analyze('this one talks about issue JIRA-1234', nil)
- expect(subject.issues).to eq([{ project: nil, id: 'JIRA-1234' }])
+ subject.analyze('this one talks about issue JIRA-1234')
+ expect(subject.references[:issue]).to eq([[project, 'JIRA-1234']])
end
it 'extracts merge request references' do
- subject.analyze("and here's !43, a merge request", nil)
- expect(subject.merge_requests).to eq([{ project: nil, id: '43' }])
+ subject.analyze("and here's !43, a merge request")
+ expect(subject.references[:merge_request]).to eq([[project, '43']])
end
it 'extracts snippet ids' do
- subject.analyze('snippets like $12 get extracted as well', nil)
- expect(subject.snippets).to eq([{ project: nil, id: '12' }])
+ subject.analyze('snippets like $12 get extracted as well')
+ expect(subject.references[:snippet]).to eq([[project, '12']])
end
it 'extracts commit shas' do
- subject.analyze('commit shas 98cf0ae3 are pulled out as Strings', nil)
- expect(subject.commits).to eq([{ project: nil, id: '98cf0ae3' }])
+ subject.analyze('commit shas 98cf0ae3 are pulled out as Strings')
+ expect(subject.references[:commit]).to eq([[project, '98cf0ae3']])
end
it 'extracts commit ranges' do
- subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4', nil)
- expect(subject.commit_ranges).to eq([{ project: nil, id: '98cf0ae3...98cf0ae4' }])
+ subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4')
+ expect(subject.references[:commit_range]).to eq([[project, '98cf0ae3...98cf0ae4']])
end
it 'extracts multiple references and preserves their order' do
- subject.analyze('@me and @you both care about this', nil)
- expect(subject.users).to eq([
- { project: nil, id: 'me' },
- { project: nil, id: 'you' }
+ subject.analyze('@me and @you both care about this')
+ expect(subject.references[:user]).to eq([
+ [project, 'me'],
+ [project, 'you']
])
end
it 'leaves the original note unmodified' do
text = 'issue #123 is just the worst, @user'
- subject.analyze(text, nil)
+ subject.analyze(text)
expect(text).to eq('issue #123 is just the worst, @user')
end
it 'extracts no references for <pre>..</pre> blocks' do
- subject.analyze("<pre>def puts '#1 issue'\nend\n</pre>```", nil)
+ subject.analyze("<pre>def puts '#1 issue'\nend\n</pre>```")
expect(subject.issues).to be_blank
end
it 'extracts no references for <code>..</code> blocks' do
- subject.analyze("<code>def puts '!1 request'\nend\n</code>```", nil)
+ subject.analyze("<code>def puts '!1 request'\nend\n</code>```")
expect(subject.merge_requests).to be_blank
end
it 'extracts no references for code blocks with language' do
- subject.analyze("this code:\n```ruby\ndef puts '#1 issue'\nend\n```", nil)
+ subject.analyze("this code:\n```ruby\ndef puts '#1 issue'\nend\n```")
expect(subject.issues).to be_blank
end
it 'extracts issue references for invalid code blocks' do
- subject.analyze('test: ```this one talks about issue #1234```', nil)
- expect(subject.issues).to eq([{ project: nil, id: '1234' }])
+ subject.analyze('test: ```this one talks about issue #1234```')
+ expect(subject.references[:issue]).to eq([[project, '1234']])
end
it 'handles all possible kinds of references' do
@@ -75,83 +78,79 @@ describe Gitlab::ReferenceExtractor do
expect(subject).to respond_to(*accessors)
end
- context 'with a project' do
- let(:project) { create(:project) }
-
- it 'accesses valid user objects on the project team' do
- @u_foo = create(:user, username: 'foo')
- @u_bar = create(:user, username: 'bar')
- create(:user, username: 'offteam')
+ it 'accesses valid user objects' do
+ @u_foo = create(:user, username: 'foo')
+ @u_bar = create(:user, username: 'bar')
+ @u_offteam = create(:user, username: 'offteam')
- project.team << [@u_foo, :reporter]
- project.team << [@u_bar, :guest]
+ project.team << [@u_foo, :reporter]
+ project.team << [@u_bar, :guest]
- subject.analyze('@foo, @baduser, @bar, and @offteam', project)
- expect(subject.users_for(project)).to eq([@u_foo, @u_bar])
- end
+ subject.analyze('@foo, @baduser, @bar, and @offteam')
+ expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam])
+ end
- it 'accesses valid issue objects' do
- @i0 = create(:issue, project: project)
- @i1 = create(:issue, project: project)
+ it 'accesses valid issue objects' do
+ @i0 = create(:issue, project: project)
+ @i1 = create(:issue, project: project)
- subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project)
- expect(subject.issues_for(project)).to eq([@i0, @i1])
- end
+ subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.")
+ expect(subject.issues).to eq([@i0, @i1])
+ end
- it 'accesses valid merge requests' do
- @m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa')
- @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb')
+ it 'accesses valid merge requests' do
+ @m0 = create(:merge_request, source_project: project, target_project: project, source_branch: 'aaa')
+ @m1 = create(:merge_request, source_project: project, target_project: project, source_branch: 'bbb')
- subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.", project)
- expect(subject.merge_requests_for(project)).to eq([@m1, @m0])
- end
+ subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.")
+ expect(subject.merge_requests).to eq([@m1, @m0])
+ end
- it 'accesses valid snippets' do
- @s0 = create(:project_snippet, project: project)
- @s1 = create(:project_snippet, project: project)
- @s2 = create(:project_snippet)
+ it 'accesses valid snippets' do
+ @s0 = create(:project_snippet, project: project)
+ @s1 = create(:project_snippet, project: project)
+ @s2 = create(:project_snippet)
- subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}", project)
- expect(subject.snippets_for(project)).to eq([@s0, @s1])
- end
+ subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}")
+ expect(subject.snippets).to eq([@s0, @s1])
+ end
- it 'accesses valid commits' do
- commit = project.repository.commit('master')
+ it 'accesses valid commits' do
+ commit = project.repository.commit('master')
- subject.analyze("this references commits #{commit.sha[0..6]} and 012345",
- project)
- extracted = subject.commits_for(project)
- expect(extracted.size).to eq(1)
- expect(extracted[0].sha).to eq(commit.sha)
- expect(extracted[0].message).to eq(commit.message)
- end
+ subject.analyze("this references commits #{commit.sha[0..6]} and 012345")
+ extracted = subject.commits
+ expect(extracted.size).to eq(1)
+ expect(extracted[0].sha).to eq(commit.sha)
+ expect(extracted[0].message).to eq(commit.message)
+ end
- it 'accesses valid commit ranges' do
- commit = project.repository.commit('master')
- earlier_commit = project.repository.commit('master~2')
+ it 'accesses valid commit ranges' do
+ commit = project.repository.commit('master')
+ earlier_commit = project.repository.commit('master~2')
- subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}",
- project)
- extracted = subject.commit_ranges_for(project)
- expect(extracted.size).to eq(1)
- expect(extracted[0][0].sha).to eq(earlier_commit.sha)
- expect(extracted[0][0].message).to eq(earlier_commit.message)
- expect(extracted[0][1].sha).to eq(commit.sha)
- expect(extracted[0][1].message).to eq(commit.message)
- end
+ subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}")
+ extracted = subject.commit_ranges
+ expect(extracted.size).to eq(1)
+ expect(extracted[0][0].sha).to eq(earlier_commit.sha)
+ expect(extracted[0][0].message).to eq(earlier_commit.message)
+ expect(extracted[0][1].sha).to eq(commit.sha)
+ expect(extracted[0][1].message).to eq(commit.message)
end
context 'with a project with an underscore' do
- let(:project) { create(:project, path: 'test_project') }
- let(:issue) { create(:issue, project: project) }
+ let(:other_project) { create(:project, path: 'test_project') }
+ let(:issue) { create(:issue, project: other_project) }
+
+ before do
+ other_project.team << [project.creator, :developer]
+ end
it 'handles project issue references' do
- subject.analyze("this refers issue #{project.path_with_namespace}##{issue.iid}",
- project)
- extracted = subject.issues_for(project)
+ subject.analyze("this refers issue #{other_project.path_with_namespace}##{issue.iid}")
+ extracted = subject.issues
expect(extracted.size).to eq(1)
expect(extracted).to eq([issue])
end
-
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 1db9f15b790..727884c41c5 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
describe Gitlab::Regex do
- describe 'path regex' do
- it { expect('gitlab-ce').to match(Gitlab::Regex.path_regex) }
- it { expect('gitlab_git').to match(Gitlab::Regex.path_regex) }
- it { expect('_underscore.js').to match(Gitlab::Regex.path_regex) }
- it { expect('100px.com').to match(Gitlab::Regex.path_regex) }
- it { expect('?gitlab').not_to match(Gitlab::Regex.path_regex) }
- it { expect('git lab').not_to match(Gitlab::Regex.path_regex) }
- it { expect('gitlab.git').not_to match(Gitlab::Regex.path_regex) }
+ describe 'project path regex' do
+ it { expect('gitlab-ce').to match(Gitlab::Regex.project_path_regex) }
+ it { expect('gitlab_git').to match(Gitlab::Regex.project_path_regex) }
+ it { expect('_underscore.js').to match(Gitlab::Regex.project_path_regex) }
+ it { expect('100px.com').to match(Gitlab::Regex.project_path_regex) }
+ it { expect('?gitlab').not_to match(Gitlab::Regex.project_path_regex) }
+ it { expect('git lab').not_to match(Gitlab::Regex.project_path_regex) }
+ it { expect('gitlab.git').not_to match(Gitlab::Regex.project_path_regex) }
end
describe 'project name regex' do
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index ba42f9e5c70..b297fbd5119 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -7,9 +7,12 @@ describe Notify do
let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name }
let(:gitlab_sender) { Gitlab.config.gitlab.email_from }
+ let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to }
let(:recipient) { create(:user, email: 'recipient@example.com') }
let(:project) { create(:project) }
+ around(:each) { ActionMailer::Base.deliveries.clear }
+
before(:each) do
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
@@ -27,6 +30,11 @@ describe Notify do
expect(sender.display_name).to eq(gitlab_sender_display_name)
expect(sender.address).to eq(gitlab_sender)
end
+
+ it 'has a Reply-To address' do
+ reply_to = subject.header[:reply_to].addresses
+ expect(reply_to).to eq([gitlab_sender_reply_to])
+ end
end
shared_examples 'an email starting a new thread' do |message_id_prefix|
@@ -183,13 +191,6 @@ describe Notify do
context 'for issues' do
let(:issue) { create(:issue, author: current_user, assignee: assignee, project: project) }
let(:issue_with_description) { create(:issue, author: current_user, assignee: assignee, project: project, description: Faker::Lorem.sentence) }
- let(:issue_with_image) do
- create(:issue,
- author: current_user,
- assignee: assignee,
- project: project,
- description: "![test](#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/12345/test.jpg)")
- end
describe 'that are new' do
subject { Notify.new_issue_email(issue.assignee_id, issue.id) }
@@ -214,22 +215,6 @@ describe Notify do
end
end
- describe 'that contain images' do
- let(:png) { File.read("#{Rails.root}/spec/fixtures/dk.png") }
- let(:png_encoded) { Base64::encode64(png) }
-
- before :each do
- file_path = File.join(Rails.root, 'public', 'uploads', issue_with_image.project.path_with_namespace, '12345/test.jpg')
- allow(File).to receive(:file?).with(file_path).and_return(true)
- allow(File).to receive(:read).with(file_path).and_return(png)
- end
-
- subject { Notify.new_issue_email(issue_with_image.assignee_id, issue_with_image.id) }
- it 'replaces attached images with inline images' do
- is_expected.to have_body_text URI.encode(png_encoded)
- end
- end
-
describe 'that have been reassigned' do
subject { Notify.reassigned_issue_email(recipient.id, issue.id, previous_assignee.id, current_user) }
@@ -294,14 +279,6 @@ describe Notify do
let(:merge_author) { create(:user) }
let(:merge_request) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project) }
let(:merge_request_with_description) { create(:merge_request, author: current_user, assignee: assignee, source_project: project, target_project: project, description: Faker::Lorem.sentence) }
- let(:merge_request_with_image) do
- create(:merge_request,
- author: current_user,
- assignee: assignee,
- source_project: project,
- target_project: project,
- description: "![test](#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/12345/test.jpg)")
- end
describe 'that are new' do
subject { Notify.new_merge_request_email(merge_request.assignee_id, merge_request.id) }
@@ -338,22 +315,6 @@ describe Notify do
end
end
- describe 'that are new and contain contain images in the description' do
- let(:png) {File.read("#{Rails.root}/spec/fixtures/dk.png")}
- let(:png_encoded) { Base64::encode64(png) }
-
- before :each do
- file_path = File.join(Rails.root, 'public', 'uploads', merge_request_with_image.project.path_with_namespace, '/12345/test.jpg')
- allow(File).to receive(:file?).with(file_path).and_return(true)
- allow(File).to receive(:read).with(file_path).and_return(png)
- end
-
- subject { Notify.new_merge_request_email(merge_request_with_image.assignee_id, merge_request_with_image.id) }
- it 'replaces attached images with inline images' do
- is_expected.to have_body_text URI.encode(png_encoded)
- end
- end
-
describe 'that are reassigned' do
subject { Notify.reassigned_merge_request_email(recipient.id, merge_request.id, previous_assignee.id, current_user.id) }
@@ -462,12 +423,9 @@ describe Notify do
describe 'project access changed' do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:project_member) do
- create(:project_member,
- project: project,
- user: user)
- end
-
+ let(:project_member) { create(:project_member,
+ project: project,
+ user: user) }
subject { Notify.project_access_granted_email(project_member.id) }
it_behaves_like 'an email sent from GitLab'
@@ -507,32 +465,6 @@ describe Notify do
end
end
- describe 'on a commit that contains an image' do
- let(:commit) { project.repository.commit }
- let(:note_with_image) do
- create(:note,
- project: project,
- author: note_author,
- note: "![test](#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/uploads/12345/test.jpg)")
- end
-
- let(:png) {File.read("#{Rails.root}/spec/fixtures/dk.png")}
- let(:png_encoded) { Base64::encode64(png) }
-
- before :each do
- file_path = File.join(Rails.root, 'public', 'uploads', note_with_image.project.path_with_namespace, '12345/test.jpg')
- allow(File).to receive(:file?).with(file_path).and_return(true)
- allow(File).to receive(:read).with(file_path).and_return(png)
- allow(Note).to receive(:find).with(note_with_image.id).and_return(note_with_image)
- allow(note_with_image).to receive(:noteable).and_return(commit)
- end
-
- subject { Notify.note_commit_email(recipient.id, note_with_image.id) }
- it 'replaces attached images with inline images' do
- is_expected.to have_body_text URI.encode(png_encoded)
- end
- end
-
describe 'on a commit' do
let(:commit) { project.repository.commit }
@@ -793,6 +725,11 @@ describe Notify do
sender = subject.header[:from].addrs[0]
expect(sender.address).to eq(user.email)
end
+
+ it "is set to reply to the committer email" do
+ sender = subject.header[:reply_to].addrs[0]
+ expect(sender.address).to eq(user.email)
+ end
end
context "when the committer email domain is not completely within the GitLab domain" do
@@ -806,6 +743,11 @@ describe Notify do
sender = subject.header[:from].addrs[0]
expect(sender.address).to eq(gitlab_sender)
end
+
+ it "is set to reply to the default email" do
+ sender = subject.header[:reply_to].addrs[0]
+ expect(sender.address).to eq(gitlab_sender_reply_to)
+ end
end
context "when the committer email domain is outside the GitLab domain" do
@@ -819,6 +761,11 @@ describe Notify do
sender = subject.header[:from].addrs[0]
expect(sender.address).to eq(gitlab_sender)
end
+
+ it "is set to reply to the default email" do
+ sender = subject.header[:reply_to].addrs[0]
+ expect(sender.address).to eq(gitlab_sender_reply_to)
+ end
end
end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 8b3d88640da..11cc7762ce4 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -14,7 +14,7 @@ describe Commit do
message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.'
allow(commit).to receive(:safe_message).and_return(message)
- expect(commit.title).to eq("#{message[0..79]}&hellip;")
+ expect(commit.title).to eq("#{message[0..79]}…")
end
it "truncates a message with a newline before 80 characters at the newline" do
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index f351aab9238..7032b777144 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -28,17 +28,32 @@ describe DeployKeysProject do
let(:deploy_key) { subject.deploy_key }
context "when the deploy key is only used by this project" do
- it "destroys the deploy key" do
- subject.destroy
+ context "when the deploy key is public" do
+ before do
+ deploy_key.update_attribute(:public, true)
+ end
- expect {
- deploy_key.reload
- }.to raise_error(ActiveRecord::RecordNotFound)
+ it "doesn't destroy the deploy key" do
+ subject.destroy
+
+ expect {
+ deploy_key.reload
+ }.not_to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context "when the deploy key is private" do
+ it "destroys the deploy key" do
+ subject.destroy
+
+ expect {
+ deploy_key.reload
+ }.to raise_error(ActiveRecord::RecordNotFound)
+ end
end
end
context "when the deploy key is used by more than one project" do
-
let!(:other_project) { create(:project) }
before do
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index a212b95a7d6..2fb651bef1b 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -58,12 +58,17 @@ describe Key do
expect(build(:key)).to be_valid
end
- it "rejects the unfingerprintable key (contains space in middle)" do
- expect(build(:key_with_a_space_in_the_middle)).not_to be_valid
+ it 'rejects an unfingerprintable key that contains a space' do
+ key = build(:key)
+
+ # Not always the middle, but close enough
+ key.key = key.key[0..100] + ' ' + key.key[100..-1]
+
+ expect(key).not_to be_valid
end
- it "rejects the unfingerprintable key (not a key)" do
- expect(build(:invalid_key)).not_to be_valid
+ it 'rejects the unfingerprintable key (not a key)' do
+ expect(build(:key, key: 'ssh-rsa an-invalid-key==')).not_to be_valid
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
new file mode 100644
index 00000000000..56d030a03b3
--- /dev/null
+++ b/spec/models/member_spec.rb
@@ -0,0 +1,148 @@
+require 'spec_helper'
+
+describe Member do
+ describe "Associations" do
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe "Validation" do
+ subject { Member.new(access_level: Member::GUEST) }
+
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:source) }
+ it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
+
+ context "when an invite email is provided" do
+ let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it "doesn't require a user" do
+ expect(member).to be_valid
+ end
+
+ it "requires a valid invite email" do
+ member.invite_email = "nope"
+
+ expect(member).not_to be_valid
+ end
+
+ it "requires a unique invite email scoped to this source" do
+ create(:project_member, source: member.source, invite_email: member.invite_email)
+
+ expect(member).not_to be_valid
+ end
+
+ it "is valid otherwise" do
+ expect(member).to be_valid
+ end
+ end
+
+ context "when an invite email is not provided" do
+ let(:member) { build(:project_member) }
+
+ it "requires a user" do
+ member.user = nil
+
+ expect(member).not_to be_valid
+ end
+
+ it "is valid otherwise" do
+ expect(member).to be_valid
+ end
+ end
+ end
+
+ describe "Delegate methods" do
+ it { is_expected.to respond_to(:user_name) }
+ it { is_expected.to respond_to(:user_email) }
+ end
+
+ describe ".add_user" do
+ let!(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ context "when called with a user id" do
+ it "adds the user as a member" do
+ Member.add_user(project.project_members, user.id, ProjectMember::MASTER)
+
+ expect(project.users).to include(user)
+ end
+ end
+
+ context "when called with a user object" do
+ it "adds the user as a member" do
+ Member.add_user(project.project_members, user, ProjectMember::MASTER)
+
+ expect(project.users).to include(user)
+ end
+ end
+
+ context "when called with a known user email" do
+ it "adds the user as a member" do
+ Member.add_user(project.project_members, user.email, ProjectMember::MASTER)
+
+ expect(project.users).to include(user)
+ end
+ end
+
+ context "when called with an unknown user email" do
+ it "adds a member invite" do
+ Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER)
+
+ expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com")
+ end
+ end
+ end
+
+ describe "#accept_invite!" do
+ let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+ let(:user) { create(:user) }
+
+ it "resets the invite token" do
+ member.accept_invite!(user)
+
+ expect(member.invite_token).to be_nil
+ end
+
+ it "sets the invite accepted timestamp" do
+ member.accept_invite!(user)
+
+ expect(member.invite_accepted_at).not_to be_nil
+ end
+
+ it "sets the user" do
+ member.accept_invite!(user)
+
+ expect(member.user).to eq(user)
+ end
+
+ it "calls #after_accept_invite" do
+ expect(member).to receive(:after_accept_invite)
+
+ member.accept_invite!(user)
+ end
+ end
+
+ describe "#decline_invite!" do
+ let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it "destroys the member" do
+ member.decline_invite!
+
+ expect(member).to be_destroyed
+ end
+
+ it "calls #after_decline_invite" do
+ expect(member).to receive(:after_decline_invite)
+
+ member.decline_invite!
+ end
+ end
+
+ describe "#generate_invite_token" do
+ let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) }
+
+ it "sets the invite token" do
+ expect { member.generate_invite_token }.to change { member.invite_token}
+ end
+ end
+end
diff --git a/spec/models/members_spec.rb b/spec/models/members_spec.rb
deleted file mode 100644
index dfd3f7feb6b..00000000000
--- a/spec/models/members_spec.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'spec_helper'
-
-describe Member do
- describe "Associations" do
- it { is_expected.to belong_to(:user) }
- end
-
- describe "Validation" do
- subject { Member.new(access_level: Member::GUEST) }
-
- it { is_expected.to validate_presence_of(:user) }
- it { is_expected.to validate_presence_of(:source) }
- it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) }
- end
-
- describe "Delegate methods" do
- it { is_expected.to respond_to(:user_name) }
- it { is_expected.to respond_to(:user_email) }
- end
-end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index ed6845c82cc..e87432fdf62 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -33,8 +33,6 @@ describe Namespace do
it { is_expected.to respond_to(:to_param) }
end
- it { expect(Namespace.global_id).to eq('GLN') }
-
describe :to_param do
it { expect(namespace.to_param).to eq(namespace.path) }
end
@@ -85,4 +83,14 @@ describe Namespace do
it { expect(Namespace.find_by_path_or_name('WOW')).to eq(@namespace) }
it { expect(Namespace.find_by_path_or_name('unknown')).to eq(nil) }
end
+
+ describe ".clean_path" do
+
+ let!(:user) { create(:user, username: "johngitlab-etc") }
+ let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") }
+
+ it "cleans the path and makes sure it's available" do
+ expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
+ end
+ end
end
diff --git a/spec/models/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 13c8d54a2af..13c8d54a2af 100644
--- a/spec/models/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 9f29fbe12b0..e987241f3ca 100644
--- a/spec/models/project_services/buildbox_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -19,7 +19,7 @@
require 'spec_helper'
-describe BuildboxService do
+describe BuildkiteService do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -32,7 +32,7 @@ describe BuildboxService do
default_branch: 'default-brancho'
)
- @service = BuildboxService.new
+ @service = BuildkiteService.new
@service.stub(
project: @project,
service_hook: true,
diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb
index 610f33c5823..6a557d839ca 100644
--- a/spec/models/project_services/gitlab_ci_service_spec.rb
+++ b/spec/models/project_services/gitlab_ci_service_spec.rb
@@ -46,4 +46,25 @@ describe GitlabCiService do
it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")}
end
end
+
+ describe "Fork registration" do
+ before do
+ @old_project = create(:empty_project)
+ @project = create(:empty_project)
+ @user = create(:user)
+
+ @service = GitlabCiService.new
+ @service.stub(
+ service_hook: true,
+ project_url: 'http://ci.gitlab.org/projects/2',
+ token: 'verySecret',
+ project: @old_project
+ )
+ end
+
+ it "performs http reuquest to ci" do
+ stub_request(:post, "http://ci.gitlab.org/api/v1/forks")
+ @service.fork_registration(@project, @user.private_token)
+ end
+ end
end
diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
index 959044dc727..f94bef5c365 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -31,6 +31,7 @@ describe GitlabIssueTrackerService do
context 'with absolute urls' do
before do
+ GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root"
@service = project.create_gitlab_issue_tracker_service(active: true)
end
@@ -39,15 +40,15 @@ describe GitlabIssueTrackerService do
end
it 'should give the correct path' do
- expect(@service.project_url).to eq("/#{project.path_with_namespace}/issues")
- expect(@service.new_issue_url).to eq("/#{project.path_with_namespace}/issues/new")
- expect(@service.issue_url(432)).to eq("/#{project.path_with_namespace}/issues/432")
+ expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues")
+ expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new")
+ expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432")
end
end
- context 'with enabled relative urls' do
+ context 'with relative urls' do
before do
- Settings.gitlab.stub(:relative_url_root).and_return("/gitlab/root")
+ GitlabIssueTrackerService.default_url_options[:script_name] = "/gitlab/root"
@service = project.create_gitlab_issue_tracker_service(active: true)
end
@@ -56,9 +57,9 @@ describe GitlabIssueTrackerService do
end
it 'should give the correct path' do
- expect(@service.project_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
- expect(@service.new_issue_url).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
- expect(@service.issue_url(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
+ expect(@service.project_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues")
+ expect(@service.new_issue_path).to eq("/gitlab/root/#{project.path_with_namespace}/issues/new")
+ expect(@service.issue_path(432)).to eq("/gitlab/root/#{project.path_with_namespace}/issues/432")
end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 10e90cae143..24384e8bf22 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -307,16 +307,6 @@ describe User do
end
end
- describe ".clean_username" do
-
- let!(:user) { create(:user, username: "johngitlab-etc") }
- let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") }
-
- it "cleans a username and makes sure it's available" do
- expect(User.clean_username("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
- end
- end
-
describe 'all_ssh_keys' do
it { is_expected.to have_many(:keys).dependent(:destroy) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index f28dfea3ccf..cc387378d3a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -57,7 +57,14 @@ describe API::API, api: true do
expect(json_response.first['name']).to eq(project.name)
expect(json_response.first['owner']['username']).to eq(user.username)
end
-
+
+ it 'should include the project labels as the tag_list' do
+ get api('/projects', user)
+ response.status.should == 200
+ json_response.should be_an Array
+ json_response.first.keys.should include('tag_list')
+ end
+
context 'and using search' do
it 'should return searched project' do
get api('/projects', user), { search: project.name }
@@ -247,12 +254,12 @@ describe API::API, api: true do
expect(json_response['message']['name']).to eq([
'can\'t be blank',
'is too short (minimum is 0 characters)',
- Gitlab::Regex.project_regex_message
+ Gitlab::Regex.project_name_regex_message
])
expect(json_response['message']['path']).to eq([
'can\'t be blank',
'is too short (minimum is 0 characters)',
- Gitlab::Regex.send(:default_regex_message)
+ Gitlab::Regex.send(:project_path_regex_message)
])
end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index 729970153d1..09a79553f72 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -11,8 +11,6 @@ describe API::API, api: true do
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
- before { project.team << [user, :reporter] }
-
describe "GET /projects/:id/repository/tags" do
it "should return an array of project tags" do
get api("/projects/#{project.id}/repository/tags", user)
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 081400cdedd..e6d5545f812 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -140,7 +140,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.send(:default_regex_message)])
+ to eq([Gitlab::Regex.send(:namespace_regex_message)])
end
it "shouldn't available for non admin users" do
@@ -266,7 +266,7 @@ describe API::API, api: true do
expect(json_response['message']['projects_limit']).
to eq(['must be greater than or equal to 0'])
expect(json_response['message']['username']).
- to eq([Gitlab::Regex.send(:default_regex_message)])
+ to eq([Gitlab::Regex.send(:namespace_regex_message)])
end
context "with existing user" do
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index d9bd91f5990..042352311da 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -168,12 +168,11 @@ end
# project_deploy_keys GET /:project_id/deploy_keys(.:format) deploy_keys#index
# POST /:project_id/deploy_keys(.:format) deploy_keys#create
# new_project_deploy_key GET /:project_id/deploy_keys/new(.:format) deploy_keys#new
-# edit_project_deploy_key GET /:project_id/deploy_keys/:id/edit(.:format) deploy_keys#edit
# project_deploy_key GET /:project_id/deploy_keys/:id(.:format) deploy_keys#show
-# PUT /:project_id/deploy_keys/:id(.:format) deploy_keys#update
# DELETE /:project_id/deploy_keys/:id(.:format) deploy_keys#destroy
describe Projects::DeployKeysController, 'routing' do
it_behaves_like 'RESTful project resources' do
+ let(:actions) { [:index, :show, :new, :create] }
let(:controller) { 'deploy_keys' }
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index d4915b51952..e219a57c29e 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -64,50 +64,35 @@ describe SnippetsController, "routing" do
end
end
-# help GET /help(.:format) help#index
-# help_permissions GET /help/permissions(.:format) help#permissions
-# help_workflow GET /help/workflow(.:format) help#workflow
-# help_api GET /help/api(.:format) help#api
-# help_web_hooks GET /help/web_hooks(.:format) help#web_hooks
-# help_system_hooks GET /help/system_hooks(.:format) help#system_hooks
-# help_markdown GET /help/markdown(.:format) help#markdown
-# help_ssh GET /help/ssh(.:format) help#ssh
-# help_raketasks GET /help/raketasks(.:format) help#raketasks
+# help GET /help(.:format) help#index
+# help_page GET /help/:category/:file(.:format) help#show {:category=>/.*/, :file=>/[^\/\.]+/}
+# help_shortcuts GET /help/shortcuts(.:format) help#shortcuts
+# help_ui GET /help/ui(.:format) help#ui
describe HelpController, "routing" do
it "to #index" do
expect(get("/help")).to route_to('help#index')
end
- it "to #permissions" do
- expect(get("/help/permissions/permissions")).to route_to('help#show', category: "permissions", file: "permissions")
- end
-
- it "to #workflow" do
- expect(get("/help/workflow/README")).to route_to('help#show', category: "workflow", file: "README")
- end
-
- it "to #api" do
- expect(get("/help/api/README")).to route_to('help#show', category: "api", file: "README")
- end
-
- it "to #web_hooks" do
- expect(get("/help/web_hooks/web_hooks")).to route_to('help#show', category: "web_hooks", file: "web_hooks")
- end
-
- it "to #system_hooks" do
- expect(get("/help/system_hooks/system_hooks")).to route_to('help#show', category: "system_hooks", file: "system_hooks")
- end
+ it 'to #show' do
+ path = '/help/markdown/markdown.md'
+ expect(get(path)).to route_to('help#show',
+ category: 'markdown',
+ file: 'markdown',
+ format: 'md')
- it "to #markdown" do
- expect(get("/help/markdown/markdown")).to route_to('help#show',category: "markdown", file: "markdown")
+ path = '/help/workflow/protected_branches/protected_branches1.png'
+ expect(get(path)).to route_to('help#show',
+ category: 'workflow/protected_branches',
+ file: 'protected_branches1',
+ format: 'png')
end
- it "to #ssh" do
- expect(get("/help/ssh/README")).to route_to('help#show', category: "ssh", file: "README")
+ it 'to #shortcuts' do
+ expect(get('/help/shortcuts')).to route_to('help#shortcuts')
end
- it "to #raketasks" do
- expect(get("/help/raketasks/README")).to route_to('help#show', category: "raketasks", file: "README")
+ it 'to #ui' do
+ expect(get('/help/ui')).to route_to('help#ui')
end
end
diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb
new file mode 100644
index 00000000000..f168a913976
--- /dev/null
+++ b/spec/services/archive_repository_service_spec.rb
@@ -0,0 +1,93 @@
+require 'spec_helper'
+
+describe ArchiveRepositoryService do
+ let(:project) { create(:project) }
+ subject { ArchiveRepositoryService.new(project, "master", "zip") }
+
+ describe "#execute" do
+ it "cleans old archives" do
+ expect(project.repository).to receive(:clean_old_archives)
+
+ subject.execute(timeout: 0.0)
+ end
+
+ context "when the repository doesn't have an archive file path" do
+ before do
+ allow(project.repository).to receive(:archive_file_path).and_return(nil)
+ end
+
+ it "raises an error" do
+ expect {
+ subject.execute(timeout: 0.0)
+ }.to raise_error
+ end
+ end
+
+ context "when the repository has an archive file path" do
+ let(:file_path) { "/archive.zip" }
+ let(:pid_file_path) { "/archive.zip.pid" }
+
+ before do
+ allow(project.repository).to receive(:archive_file_path).and_return(file_path)
+ allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path)
+ end
+
+ context "when the archive file already exists" do
+ before do
+ allow(File).to receive(:exist?).with(file_path).and_return(true)
+ end
+
+ it "returns the file path" do
+ expect(subject.execute(timeout: 0.0)).to eq(file_path)
+ end
+ end
+
+ context "when the archive file doesn't exist yet" do
+ before do
+ allow(File).to receive(:exist?).with(file_path).and_return(false)
+ allow(File).to receive(:exist?).with(pid_file_path).and_return(true)
+ end
+
+ context "when the archive pid file doesn't exist yet" do
+ before do
+ allow(File).to receive(:exist?).with(pid_file_path).and_return(false)
+ end
+
+ it "queues the RepositoryArchiveWorker" do
+ expect(RepositoryArchiveWorker).to receive(:perform_async)
+
+ subject.execute(timeout: 0.0)
+ end
+ end
+
+ context "when the archive pid file already exists" do
+ it "doesn't queue the RepositoryArchiveWorker" do
+ expect(RepositoryArchiveWorker).not_to receive(:perform_async)
+
+ subject.execute(timeout: 0.0)
+ end
+ end
+
+ context "when the archive file exists after a little while" do
+ before do
+ Thread.new do
+ sleep 0.1
+ allow(File).to receive(:exist?).with(file_path).and_return(true)
+ end
+ end
+
+ it "returns the file path" do
+ expect(subject.execute(timeout: 0.2)).to eq(file_path)
+ end
+ end
+
+ context "when the archive file doesn't exist after the timeout" do
+ it "returns nil" do
+ expect(subject.execute(timeout: 0.0)).to eq(nil)
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index e55a2e3f8a0..c9025bdf133 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -40,6 +40,17 @@ describe Projects::ForkService do
expect(@to_project.errors[:base]).not_to include("Fork transaction failed.")
end
end
+
+ context 'GitLab CI is enabled' do
+ it "calls fork registrator for CI" do
+ @from_project.build_missing_services
+ @from_project.gitlab_ci_service.update_attributes(active: true)
+
+ expect(ForkRegistrationWorker).to receive(:perform_async)
+
+ fork_project(@from_project, @to_user)
+ end
+ end
end
describe :fork_to_namespace do
@@ -89,7 +100,8 @@ describe Projects::ForkService do
def fork_project(from_project, user, fork_success = true, params = {})
context = Projects::ForkService.new(from_project, user, params)
- shell = double('gitlab_shell').stub(fork_repository: fork_success)
+ shell = double('gitlab_shell')
+ shell.stub(fork_repository: fork_success)
context.stub(gitlab_shell: shell)
context.execute
end
diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb
index fc34b456482..e5c47015a03 100644
--- a/spec/services/projects/upload_service_spec.rb
+++ b/spec/services/projects/upload_service_spec.rb
@@ -67,6 +67,16 @@ describe Projects::UploadService do
it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
it { expect(@link_to_file['url']).to match('doc_sample.txt') }
end
+
+ context 'for too large a file' do
+ before do
+ txt = fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain')
+ allow(txt).to receive(:size) { 1000.megabytes.to_i }
+ @link_to_file = upload_file(@project.repository, txt)
+ end
+
+ it { expect(@link_to_file).to eq(nil) }
+ end
end
def upload_file(repository, file)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index eaec2198dc8..53ccaa4fd67 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -44,3 +44,5 @@ RSpec.configure do |config|
TestEnv.init
end
end
+
+ActiveRecord::Migration.maintain_test_schema!
diff --git a/spec/support/select2_helper.rb b/spec/support/select2_helper.rb
index c7cf109a7bb..691f84f39d4 100644
--- a/spec/support/select2_helper.rb
+++ b/spec/support/select2_helper.rb
@@ -17,9 +17,9 @@ module Select2Helper
selector = options[:from]
if options[:multiple]
- execute_script("$('#{selector}').select2('val', ['#{value}']);")
+ execute_script("$('#{selector}').select2('val', ['#{value}'], true);")
else
- execute_script("$('#{selector}').select2('val', '#{value}');")
+ execute_script("$('#{selector}').select2('val', '#{value}', true);")
end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index f869488d8d8..44d70e741b2 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -85,7 +85,7 @@ module TestEnv
end
# We must copy bare repositories because we will push to them.
- system(*%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare}))
+ system(git_env, *%W(git clone -q --bare #{factory_repo_path} #{factory_repo_path_bare}))
end
def copy_repo(project)
@@ -113,4 +113,10 @@ module TestEnv
def factory_repo_name
'gitlab-test'
end
+
+ # Prevent developer git configurations from being persisted to test
+ # repositories
+ def git_env
+ {'GIT_TEMPLATE_DIR' => ''}
+ end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 8a411b7720a..a59f74c2121 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -87,7 +87,7 @@ describe 'gitlab:app namespace rake task' do
expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads/')
expect(tar_contents).to match('repositories/')
- expect(tar_contents).not_to match(/^.{4,9}[rwx]/)
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/)
end
it 'should delete temp directories' do
@@ -98,4 +98,55 @@ describe 'gitlab:app namespace rake task' do
expect(temp_dirs).to be_empty
end
end # backup_create task
+
+ describe "Skipping items" do
+ def tars_glob
+ Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
+ end
+
+ before :all do
+ @origin_cd = Dir.pwd
+
+ Rake::Task["gitlab:backup:db:create"].reenable
+ Rake::Task["gitlab:backup:repo:create"].reenable
+ Rake::Task["gitlab:backup:uploads:create"].reenable
+
+ # Record the existing backup tars so we don't touch them
+ existing_tars = tars_glob
+
+ # Redirect STDOUT and run the rake task
+ orig_stdout = $stdout
+ $stdout = StringIO.new
+ ENV["SKIP"] = "repositories"
+ run_rake_task('gitlab:backup:create')
+ $stdout = orig_stdout
+
+ @backup_tar = (tars_glob - existing_tars).first
+ end
+
+ after :all do
+ FileUtils.rm(@backup_tar)
+ Dir.chdir @origin_cd
+ end
+
+ it "does not contain skipped item" do
+ tar_contents, exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar} db uploads repositories}
+ )
+
+ expect(tar_contents).to match('db/')
+ expect(tar_contents).to match('uploads/')
+ expect(tar_contents).not_to match('repositories/')
+ end
+
+ it 'does not invoke repositories restore' do
+ Rake::Task["gitlab:shell:setup"].stub invoke: true
+ allow($stdout).to receive :write
+
+ expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
+ expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
+ expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
+ expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error
+ end
+ end
end # gitlab:app namespace
diff --git a/spec/workers/fork_registration_worker_spec.rb b/spec/workers/fork_registration_worker_spec.rb
new file mode 100644
index 00000000000..cc6f574b29c
--- /dev/null
+++ b/spec/workers/fork_registration_worker_spec.rb
@@ -0,0 +1,10 @@
+
+require 'spec_helper'
+
+describe ForkRegistrationWorker do
+ context "as a resque worker" do
+ it "reponds to #perform" do
+ expect(ForkRegistrationWorker.new).to respond_to(:perform)
+ end
+ end
+end
diff --git a/spec/workers/repository_archive_worker_spec.rb b/spec/workers/repository_archive_worker_spec.rb
new file mode 100644
index 00000000000..c2362058cfd
--- /dev/null
+++ b/spec/workers/repository_archive_worker_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe RepositoryArchiveWorker do
+ let(:project) { create(:project) }
+ subject { RepositoryArchiveWorker.new }
+
+ before do
+ allow(Project).to receive(:find).and_return(project)
+ end
+
+ describe "#perform" do
+ it "cleans old archives" do
+ expect(project.repository).to receive(:clean_old_archives)
+
+ subject.perform(project.id, "master", "zip")
+ end
+
+ context "when the repository doesn't have an archive file path" do
+ before do
+ allow(project.repository).to receive(:archive_file_path).and_return(nil)
+ end
+
+ it "doesn't archive the repo" do
+ expect(project.repository).not_to receive(:archive_repo)
+
+ subject.perform(project.id, "master", "zip")
+ end
+ end
+
+ context "when the repository has an archive file path" do
+ let(:file_path) { "/archive.zip" }
+ let(:pid_file_path) { "/archive.zip.pid" }
+
+ before do
+ allow(project.repository).to receive(:archive_file_path).and_return(file_path)
+ allow(project.repository).to receive(:archive_pid_file_path).and_return(pid_file_path)
+ end
+
+ context "when the archive file already exists" do
+ before do
+ allow(File).to receive(:exist?).with(file_path).and_return(true)
+ end
+
+ it "doesn't archive the repo" do
+ expect(project.repository).not_to receive(:archive_repo)
+
+ subject.perform(project.id, "master", "zip")
+ end
+ end
+
+ context "when the archive file doesn't exist yet" do
+ before do
+ allow(File).to receive(:exist?).with(file_path).and_return(false)
+ allow(File).to receive(:exist?).with(pid_file_path).and_return(true)
+ end
+
+ context "when the archive pid file doesn't exist yet" do
+ before do
+ allow(File).to receive(:exist?).with(pid_file_path).and_return(false)
+ end
+
+ it "archives the repo" do
+ expect(project.repository).to receive(:archive_repo)
+
+ subject.perform(project.id, "master", "zip")
+ end
+ end
+
+ context "when the archive pid file already exists" do
+ it "doesn't archive the repo" do
+ expect(project.repository).not_to receive(:archive_repo)
+
+ subject.perform(project.id, "master", "zip")
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/vendor/assets/javascripts/chart-lib.min.js b/vendor/assets/javascripts/chart-lib.min.js
index 626e6c3cdb9..3a0a2c87345 100644
--- a/vendor/assets/javascripts/chart-lib.min.js
+++ b/vendor/assets/javascripts/chart-lib.min.js
@@ -1,11 +1,11 @@
/*!
* Chart.js
* http://chartjs.org/
- * Version: 1.0.1-beta.4
+ * Version: 1.0.2
*
- * Copyright 2014 Nick Downie
+ * Copyright 2015 Nick Downie
* Released under the MIT license
* https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
*/
-(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;this.width=t.canvas.width,this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof t.define&&t.define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),x=s.radians=function(t){return t*(Math.PI/180)},S=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),x=Math.round(f/v);(x>a||a>2*x)&&!h;)if(x>a)v*=2,x=Math.round(f/v),x%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,x=Math.round(f/v)}else v/=2,x=Math.round(f/v);return h&&(x=o,v=f/x),{steps:x,stepValue:v,min:p,max:p+x*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),b=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-b.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*b.easeInBounce(2*t):.5*b.easeOutBounce(2*t-1)+.5}}),w=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=(s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),s.animationLoop=function(t,i,e,s,n,o){var a=0,h=b[e]||b.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=w(l):n.apply(o)};w(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),L=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},k=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},P(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){L(t.chart.canvas,e,i)})}),F=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},R=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),T=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},M=s.fontString=function(t,i,e){return i+" "+t+"px "+e},W=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},z=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return T(this.chart),this},stop:function(){return s.cancelAnimFrame.call(t,this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=F(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:R(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),k(this,this.events),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;switch(t.fillStyle=this.fillColor,this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}z(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=M(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=W(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){z(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?W(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=W(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(x(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(x(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/(this.valuesCount-(this.offsetGridLines?0:1)),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a);t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+S(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+S(this.lineWidth),o=this.xLabelRotation>0;t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath(),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*x(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=M(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=M(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};
-i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(t/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file
+(function(){"use strict";var t=this,i=t.Chart,e=function(t){this.canvas=t.canvas,this.ctx=t;var i=function(t,i){return t["offset"+i]?t["offset"+i]:document.defaultView.getComputedStyle(t).getPropertyValue(i)},e=this.width=i(t.canvas,"Width"),n=this.height=i(t.canvas,"Height");t.canvas.width=e,t.canvas.height=n;var e=this.width=t.canvas.width,n=this.height=t.canvas.height;return this.aspectRatio=this.width/this.height,s.retinaScale(this),this};e.defaults={global:{animation:!0,animationSteps:60,animationEasing:"easeOutQuart",showScale:!0,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n<t.length;n++)i.apply(e,[t[n],n].concat(s))}else for(var o in t)i.apply(e,[t[o],o].concat(s))},o=s.clone=function(t){var i={};return n(t,function(e,s){t.hasOwnProperty(s)&&(i[s]=e)}),i},a=s.extend=function(t){return n(Array.prototype.slice.call(arguments,1),function(i){n(i,function(e,s){i.hasOwnProperty(s)&&(t[s]=e)})}),t},h=s.merge=function(){var t=Array.prototype.slice.call(arguments,0);return t.unshift({}),a.apply(null,t)},l=s.indexOf=function(t,i){if(Array.prototype.indexOf)return t.indexOf(i);for(var e=0;e<t.length;e++)if(t[e]===i)return e;return-1},r=(s.where=function(t,i){var e=[];return s.each(t,function(t){i(t)&&e.push(t)}),e},s.findNextWhere=function(t,i,e){e||(e=-1);for(var s=e+1;s<t.length;s++){var n=t[s];if(i(n))return n}},s.findPreviousWhere=function(t,i,e){e||(e=t.length);for(var s=e-1;s>=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),-(s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)))},easeOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),s*Math.pow(2,-10*t)*Math.sin(2*(1*t-i)*Math.PI/e)+1)},easeInOutElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:2==(t/=.5)?1:(e||(e=.3*1.5),s<Math.abs(1)?(s=1,i=e/4):i=e/(2*Math.PI)*Math.asin(1/s),1>t?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)<Math.pow(e,2)},draw:function(){if(this.display){var t=this.ctx;t.beginPath(),t.arc(this.x,this.y,this.radius,0,2*Math.PI),t.closePath(),t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.fillStyle=this.fillColor,t.fill(),t.stroke()}}}),e.Arc=e.Element.extend({inRange:function(t,i){var e=s.getAngleFromPoint(this,{x:t,y:i}),n=e.angle>=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),t<this.yLabelWidth&&this.calculateXLabelRotation()},calculateXLabelRotation:function(){this.ctx.font=this.font;var t,i,e=this.ctx.measureText(this.xLabels[0]).width,s=this.ctx.measureText(this.xLabels[this.xLabels.length-1]).width;if(this.xScalePaddingRight=s/2+3,this.xScalePaddingLeft=e/2>this.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;i<this.valuesCount;i++)t=this.getPointPosition(i,d),e=this.ctx.measureText(C(this.templateString,{value:this.labels[i]})).width+5,0===i||i===this.valuesCount/2?(s=e/2,t.x+s>p&&(p=t.x+s,n=i),t.x-s<g&&(g=t.x-s,a=i)):i<this.valuesCount/2?t.x+e>p&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e<g&&(g=t.x-e,a=i);l=g,r=Math.ceil(p-this.width),o=this.getIndexAngle(n),h=this.getIndexAngle(a),c=r/Math.sin(o+Math.PI/2),u=l/Math.sin(h+Math.PI/2),c=f(c)?c:0,u=f(u)?u:0,this.drawingArea=d-(u+c)/2,this.setCenterPoint(u,c)},setCenterPoint:function(t,i){var e=this.width-i-this.drawingArea,s=t+this.drawingArea;this.xCenter=(s+e)/2,this.yCenter=this.height/2},getIndexAngle:function(t){var i=2*Math.PI/this.valuesCount;return t*i-Math.PI/2},getPointPosition:function(t,i){var e=this.getIndexAngle(t);return{x:Math.cos(e)*i+this.xCenter,y:Math.sin(e)*i+this.yCenter}},draw:function(){if(this.display){var t=this.ctx;if(n(this.yLabels,function(i,e){if(e>0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a<this.valuesCount;a++)s=this.getPointPosition(a,this.calculateCenterOffset(this.min+e*this.stepValue)),0===a?t.moveTo(s.x,s.y):t.lineTo(s.x,s.y);t.closePath(),t.stroke()}if(this.showLabels){if(t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.showLabelBackdrop){var h=t.measureText(i).width;t.fillStyle=this.backdropColor,t.fillRect(this.xCenter-h/2-this.backdropPaddingX,o-this.fontSize/2-this.backdropPaddingY,h+2*this.backdropPaddingX,this.fontSize+2*this.backdropPaddingY)}t.textAlign="center",t.textBaseline="middle",t.fillStyle=this.fontColor,t.fillText(i,this.xCenter,o)}}},this),!this.lineArc){t.lineWidth=this.angleLineWidth,t.strokeStyle=this.angleLineColor;for(var i=this.valuesCount-1;i>=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].fillColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<this.datasets.length;a++)for(i=0;i<this.datasets[a].bars.length;i++)if(this.datasets[a].bars[i].inRange(n.x,n.y))return e.each(this.datasets,o),s;return s},buildScale:function(t){var i=this,s=function(){var t=[];return i.eachBars(function(i){t.push(i.value)}),t},n={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(s(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.barShowStroke?this.options.barStrokeWidth:0,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(n,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new this.ScaleClass(n)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].bars.push(new this.BarClass({value:t,label:i,x:this.scale.calculateBarX(this.datasets.length,e,this.scale.valuesCount+1),y:this.scale.endPoint,width:this.scale.calculateBarWidth(this.datasets.length),base:this.scale.endPoint,strokeColor:this.datasets[e].strokeColor,fillColor:this.datasets[e].fillColor}))
+},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.bars.shift()},this),this.update()},reflow:function(){e.extend(this.BarClass.prototype,{y:this.scale.endPoint,base:this.scale.endPoint});var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();this.chart.ctx;this.scale.draw(i),e.each(this.datasets,function(t,s){e.each(t.bars,function(t,e){t.hasValue()&&(t.base=this.scale.endPoint,t.transition({x:this.scale.calculateBarX(this.datasets.length,s,e),y:this.scale.calculateY(t.value),width:this.scale.calculateBarWidth(this.datasets.length)},i).draw())},this)},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle)},this)}}),i.types.Doughnut.extend({name:"Pie",defaults:e.merge(s,{percentageInnerCutout:0})})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,bezierCurve:!0,bezierCurveTension:.4,pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)<Math.pow(this.radius+this.hitDetectionRadius,2)}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this),this.buildScale(t.labels),this.eachPoints(function(t,i){e.extend(t,{x:this.scale.calculateX(i),y:this.scale.endPoint}),t.save()},this)},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachPoints(function(t){t.save()}),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.datasets,function(t){e.each(t.points,function(t){t.inRange(s.x,s.y)&&i.push(t)})},this),i},buildScale:function(t){var s=this,n=function(){var t=[];return s.eachPoints(function(i){t.push(i.value)}),t},o={templateString:this.options.scaleLabel,height:this.chart.height,width:this.chart.width,ctx:this.chart.ctx,textColor:this.options.scaleFontColor,fontSize:this.options.scaleFontSize,fontStyle:this.options.scaleFontStyle,fontFamily:this.options.scaleFontFamily,valuesCount:t.length,beginAtZero:this.options.scaleBeginAtZero,integersOnly:this.options.scaleIntegersOnly,calculateYRange:function(t){var i=e.calculateScaleRange(n(),t,this.fontSize,this.beginAtZero,this.integersOnly);e.extend(this,i)},xLabels:t,font:e.fontString(this.options.scaleFontSize,this.options.scaleFontStyle,this.options.scaleFontFamily),lineWidth:this.options.scaleLineWidth,lineColor:this.options.scaleLineColor,showHorizontalLines:this.options.scaleShowHorizontalLines,showVerticalLines:this.options.scaleShowVerticalLines,gridLineWidth:this.options.scaleShowGridLines?this.options.scaleGridLineWidth:0,gridLineColor:this.options.scaleShowGridLines?this.options.scaleGridLineColor:"rgba(0,0,0,0)",padding:this.options.showScale?0:this.options.pointDotRadius+this.options.pointDotStrokeWidth,showLabels:this.options.scaleShowLabels,display:this.options.showScale};this.options.scaleOverride&&e.extend(o,{calculateYRange:e.noop,steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}),this.scale=new i.Scale(o)},addData:function(t,i){e.each(t,function(t,e){this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:this.scale.calculateX(this.scale.valuesCount+1),y:this.scale.endPoint,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.addXLabel(i),this.update()},removeData:function(){this.scale.removeXLabel(),e.each(this.datasets,function(t){t.points.shift()},this),this.update()},reflow:function(){var t=e.extend({height:this.chart.height,width:this.chart.width});this.scale.update(t)},draw:function(t){var i=t||1;this.clear();var s=this.chart.ctx,n=function(t){return null!==t.value},o=function(t,i,s){return e.findNextWhere(i,n,s)||t},a=function(t,i,s){return e.findPreviousWhere(i,n,s)||t};this.scale.draw(i),e.each(this.datasets,function(t){var h=e.where(t.points,n);e.each(t.points,function(t,e){t.hasValue()&&t.transition({y:this.scale.calculateY(t.value),x:this.scale.calculateX(e)},i)},this),this.options.bezierCurve&&e.each(h,function(t,i){var s=i>0&&i<h.length-1?this.options.bezierCurveTension:0;t.controlPoints=e.splineCurve(a(t,h,i),t,o(t,h,i),s),t.controlPoints.outer.y>this.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.y<this.scale.startPoint&&(t.controlPoints.outer.y=this.scale.startPoint),t.controlPoints.inner.y>this.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y<this.scale.startPoint&&(t.controlPoints.inner.y=this.scale.startPoint)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(h,function(t,i){if(0===i)s.moveTo(t.x,t.y);else if(this.options.bezierCurve){var e=a(t,h,i);s.bezierCurveTo(e.controlPoints.outer.x,e.controlPoints.outer.y,t.controlPoints.inner.x,t.controlPoints.inner.y,t.x,t.y)}else s.lineTo(t.x,t.y)},this),s.stroke(),this.options.datasetFill&&h.length>0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<segments.length; i++){%><li><span style="background-color:<%=segments[i].fillColor%>"></span><%if(segments[i].label){%><%=segments[i].label%><%}%></li><%}%></ul>'};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<this.segments.length-1&&(this.segments[e+1].startAngle=t.endAngle),t.draw()},this),this.scale.draw()}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers;i.Type.extend({name:"Radar",defaults:{scaleShowLine:!0,angleShowLineOut:!0,scaleShowLabels:!1,scaleBeginAtZero:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:10,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,pointHitDetectionRadius:20,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,legendTemplate:'<ul class="<%=name.toLowerCase()%>-legend"><% for (var i=0; i<datasets.length; i++){%><li><span style="background-color:<%=datasets[i].strokeColor%>"></span><%if(datasets[i].label){%><%=datasets[i].label%><%}%></li><%}%></ul>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file