summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.rubocop.yml20
-rw-r--r--.ruby-version2
-rw-r--r--CHANGELOG236
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile60
-rw-r--r--Gemfile.lock147
-rw-r--r--LICENSE2
-rw-r--r--MAINTENANCE.md2
-rw-r--r--Procfile2
-rw-r--r--README.md8
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/authbuttons/bitbucket_32.pngbin2713 -> 0 bytes
-rw-r--r--app/assets/images/authbuttons/github_32.pngbin1822 -> 0 bytes
-rw-r--r--app/assets/images/authbuttons/gitlab_32.pngbin1039 -> 0 bytes
-rw-r--r--app/assets/images/authbuttons/gitlab_64.pngbin3013 -> 6559 bytes
-rw-r--r--app/assets/images/authbuttons/google_32.pngbin1501 -> 0 bytes
-rw-r--r--app/assets/images/authbuttons/twitter_32.pngbin1311 -> 0 bytes
-rw-r--r--app/assets/javascripts/api.js.coffee67
-rw-r--r--app/assets/javascripts/application.js.coffee26
-rw-r--r--app/assets/javascripts/behaviors/taskable.js.coffee21
-rw-r--r--app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee14
-rw-r--r--app/assets/javascripts/blob/blob.js.coffee2
-rw-r--r--app/assets/javascripts/branch-graph.js.coffee2
-rw-r--r--app/assets/javascripts/calendar.js.coffee24
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js.coffee2
-rw-r--r--app/assets/javascripts/dashboard.js.coffee12
-rw-r--r--app/assets/javascripts/diff.js.coffee2
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee23
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee15
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.coffee47
-rw-r--r--app/assets/javascripts/importer_status.js.coffee10
-rw-r--r--app/assets/javascripts/issue.js.coffee41
-rw-r--r--app/assets/javascripts/issues.js.coffee2
-rw-r--r--app/assets/javascripts/merge_request.js.coffee59
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee37
-rw-r--r--app/assets/javascripts/notes.js.coffee38
-rw-r--r--app/assets/javascripts/notes_votes.js.coffee20
-rw-r--r--app/assets/javascripts/project_members.js.coffee4
-rw-r--r--app/assets/javascripts/project_users_select.js.coffee59
-rw-r--r--app/assets/javascripts/protected_branches.js.coffee6
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js.coffee10
-rw-r--r--app/assets/javascripts/shortcuts_issuable.coffee48
-rw-r--r--app/assets/javascripts/shortcuts_issueable.coffee19
-rw-r--r--app/assets/javascripts/shortcuts_navigation.coffee22
-rw-r--r--app/assets/javascripts/sidebar.js.coffee12
-rw-r--r--app/assets/javascripts/stat_graph_contributors.js.coffee4
-rw-r--r--app/assets/javascripts/stat_graph_contributors_graph.js.coffee4
-rw-r--r--app/assets/javascripts/subscription.js.coffee17
-rw-r--r--app/assets/javascripts/users_select.js.coffee89
-rw-r--r--app/assets/stylesheets/base/gl_bootstrap.scss68
-rw-r--r--app/assets/stylesheets/base/gl_variables.scss757
-rw-r--r--app/assets/stylesheets/base/mixins.scss28
-rw-r--r--app/assets/stylesheets/base/variables.scss13
-rw-r--r--app/assets/stylesheets/generic/buttons.scss15
-rw-r--r--app/assets/stylesheets/generic/calendar.scss41
-rw-r--r--app/assets/stylesheets/generic/common.scss34
-rw-r--r--app/assets/stylesheets/generic/files.scss36
-rw-r--r--app/assets/stylesheets/generic/filters.scss55
-rw-r--r--app/assets/stylesheets/generic/forms.scss8
-rw-r--r--app/assets/stylesheets/generic/header.scss (renamed from app/assets/stylesheets/pages/header.scss)111
-rw-r--r--app/assets/stylesheets/generic/highlight.scss2
-rw-r--r--app/assets/stylesheets/generic/issue_box.scss2
-rw-r--r--app/assets/stylesheets/generic/lists.scss11
-rw-r--r--app/assets/stylesheets/generic/mobile.scss36
-rw-r--r--app/assets/stylesheets/generic/selects.scss66
-rw-r--r--app/assets/stylesheets/generic/sidebar.scss (renamed from app/assets/stylesheets/generic/nav_sidebar.scss)50
-rw-r--r--app/assets/stylesheets/generic/tables.scss2
-rw-r--r--app/assets/stylesheets/generic/timeline.scss4
-rw-r--r--app/assets/stylesheets/generic/typography.scss12
-rw-r--r--app/assets/stylesheets/highlight/dark.scss8
-rw-r--r--app/assets/stylesheets/highlight/monokai.scss6
-rw-r--r--app/assets/stylesheets/highlight/solarized_dark.scss8
-rw-r--r--app/assets/stylesheets/highlight/solarized_light.scss8
-rw-r--r--app/assets/stylesheets/highlight/white.scss6
-rw-r--r--app/assets/stylesheets/pages/commit.scss3
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss45
-rw-r--r--app/assets/stylesheets/pages/diff.scss37
-rw-r--r--app/assets/stylesheets/pages/editor.scss4
-rw-r--r--app/assets/stylesheets/pages/events.scss9
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/issues.scss51
-rw-r--r--app/assets/stylesheets/pages/login.scss9
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss43
-rw-r--r--app/assets/stylesheets/pages/notes.scss34
-rw-r--r--app/assets/stylesheets/pages/profile.scss51
-rw-r--r--app/assets/stylesheets/pages/projects.scss160
-rw-r--r--app/assets/stylesheets/pages/tree.scss15
-rw-r--r--app/assets/stylesheets/pages/votes.scss35
-rw-r--r--app/assets/stylesheets/print.scss4
-rw-r--r--app/assets/stylesheets/themes/dark-theme.scss63
-rw-r--r--app/assets/stylesheets/themes/gitlab-theme.scss70
-rw-r--r--app/assets/stylesheets/themes/ui_basic.scss24
-rw-r--r--app/assets/stylesheets/themes/ui_blue.scss6
-rw-r--r--app/assets/stylesheets/themes/ui_color.scss2
-rw-r--r--app/assets/stylesheets/themes/ui_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/ui_mars.scss2
-rw-r--r--app/assets/stylesheets/themes/ui_modern.scss2
-rw-r--r--app/controllers/admin/application_controller.rb2
-rw-r--r--app/controllers/admin/application_settings_controller.rb16
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/deploy_keys_controller.rb49
-rw-r--r--app/controllers/admin/groups_controller.rb12
-rw-r--r--app/controllers/admin/keys_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb12
-rw-r--r--app/controllers/admin/services_controller.rb11
-rw-r--r--app/controllers/admin/users_controller.rb7
-rw-r--r--app/controllers/application_controller.rb55
-rw-r--r--app/controllers/autocomplete_controller.rb30
-rw-r--r--app/controllers/confirmations_controller.rb4
-rw-r--r--app/controllers/dashboard/application_controller.rb3
-rw-r--r--app/controllers/dashboard/groups_controller.rb20
-rw-r--r--app/controllers/dashboard/milestones_controller.rb6
-rw-r--r--app/controllers/dashboard/projects_controller.rb4
-rw-r--r--app/controllers/dashboard_controller.rb41
-rw-r--r--app/controllers/explore/application_controller.rb3
-rw-r--r--app/controllers/explore/groups_controller.rb8
-rw-r--r--app/controllers/explore/projects_controller.rb15
-rw-r--r--app/controllers/groups/application_controller.rb21
-rw-r--r--app/controllers/groups/avatars_controller.rb2
-rw-r--r--app/controllers/groups/group_members_controller.rb65
-rw-r--r--app/controllers/groups/milestones_controller.rb10
-rw-r--r--app/controllers/groups_controller.rb50
-rw-r--r--app/controllers/help_controller.rb77
-rw-r--r--app/controllers/import/base_controller.rb18
-rw-r--r--app/controllers/import/bitbucket_controller.rb17
-rw-r--r--app/controllers/import/github_controller.rb13
-rw-r--r--app/controllers/import/gitlab_controller.rb13
-rw-r--r--app/controllers/import/gitorious_controller.rb2
-rw-r--r--app/controllers/import/google_code_controller.rb117
-rw-r--r--app/controllers/invites_controller.rb81
-rw-r--r--app/controllers/namespaces_controller.rb20
-rw-r--r--app/controllers/oauth/applications_controller.rb9
-rw-r--r--app/controllers/oauth/authorizations_controller.rb5
-rw-r--r--app/controllers/oauth/authorized_applications_controller.rb4
-rw-r--r--app/controllers/profiles/accounts_controller.rb10
-rw-r--r--app/controllers/profiles/application_controller.rb3
-rw-r--r--app/controllers/profiles/avatars_controller.rb4
-rw-r--r--app/controllers/profiles/emails_controller.rb13
-rw-r--r--app/controllers/profiles/keys_controller.rb5
-rw-r--r--app/controllers/profiles/notifications_controller.rb10
-rw-r--r--app/controllers/profiles/passwords_controller.rb17
-rw-r--r--app/controllers/profiles_controller.rb22
-rw-r--r--app/controllers/projects/application_controller.rb14
-rw-r--r--app/controllers/projects/avatars_controller.rb4
-rw-r--r--app/controllers/projects/blame_controller.rb10
-rw-r--r--app/controllers/projects/blob_controller.rb18
-rw-r--r--app/controllers/projects/branches_controller.rb8
-rw-r--r--app/controllers/projects/commit_controller.rb14
-rw-r--r--app/controllers/projects/commits_controller.rb6
-rw-r--r--app/controllers/projects/compare_controller.rb10
-rw-r--r--app/controllers/projects/deploy_keys_controller.rb33
-rw-r--r--app/controllers/projects/forks_controller.rb5
-rw-r--r--app/controllers/projects/graphs_controller.rb8
-rw-r--r--app/controllers/projects/hooks_controller.rb2
-rw-r--r--app/controllers/projects/imports_controller.rb8
-rw-r--r--app/controllers/projects/issues_controller.rb20
-rw-r--r--app/controllers/projects/labels_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests_controller.rb55
-rw-r--r--app/controllers/projects/milestones_controller.rb10
-rw-r--r--app/controllers/projects/network_controller.rb6
-rw-r--r--app/controllers/projects/notes_controller.rb8
-rw-r--r--app/controllers/projects/project_members_controller.rb98
-rw-r--r--app/controllers/projects/protected_branches_controller.rb4
-rw-r--r--app/controllers/projects/raw_controller.rb6
-rw-r--r--app/controllers/projects/refs_controller.rb11
-rw-r--r--app/controllers/projects/repositories_controller.rb16
-rw-r--r--app/controllers/projects/services_controller.rb24
-rw-r--r--app/controllers/projects/snippets_controller.rb36
-rw-r--r--app/controllers/projects/tags_controller.rb21
-rw-r--r--app/controllers/projects/team_members_controller.rb73
-rw-r--r--app/controllers/projects/tree_controller.rb6
-rw-r--r--app/controllers/projects/uploads_controller.rb34
-rw-r--r--app/controllers/projects/wikis_controller.rb16
-rw-r--r--app/controllers/projects_controller.rb61
-rw-r--r--app/controllers/registrations_controller.rb2
-rw-r--r--app/controllers/search_controller.rb63
-rw-r--r--app/controllers/sessions_controller.rb8
-rw-r--r--app/controllers/snippets_controller.rb74
-rw-r--r--app/controllers/uploads_controller.rb48
-rw-r--r--app/controllers/users_controller.rb65
-rw-r--r--app/finders/issuable_finder.rb11
-rw-r--r--app/helpers/application_helper.rb91
-rw-r--r--app/helpers/application_settings_helper.rb17
-rw-r--r--app/helpers/blob_helper.rb8
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb7
-rw-r--r--app/helpers/dashboard_helper.rb16
-rw-r--r--app/helpers/diff_helper.rb33
-rw-r--r--app/helpers/emails_helper.rb6
-rw-r--r--app/helpers/events_helper.rb16
-rw-r--r--app/helpers/explore_helper.rb17
-rw-r--r--app/helpers/external_wiki_helper.rb11
-rw-r--r--app/helpers/gitlab_markdown_helper.rb48
-rw-r--r--app/helpers/gitlab_routing_helper.rb6
-rw-r--r--app/helpers/groups_helper.rb38
-rw-r--r--app/helpers/icons_helper.rb44
-rw-r--r--app/helpers/issues_helper.rb49
-rw-r--r--app/helpers/labels_helper.rb23
-rw-r--r--app/helpers/merge_requests_helper.rb16
-rw-r--r--app/helpers/milestones_helper.rb12
-rw-r--r--app/helpers/namespaces_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb16
-rw-r--r--app/helpers/notes_helper.rb4
-rw-r--r--app/helpers/oauth_helper.rb11
-rw-r--r--app/helpers/page_layout_helper.rb26
-rw-r--r--app/helpers/profile_helper.rb6
-rw-r--r--app/helpers/projects_helper.rb141
-rw-r--r--app/helpers/search_helper.rb8
-rw-r--r--app/helpers/selects_helper.rb33
-rw-r--r--app/helpers/submodule_helper.rb30
-rw-r--r--app/helpers/tab_helper.rb2
-rw-r--r--app/helpers/tree_helper.rb11
-rw-r--r--app/helpers/visibility_level_helper.rb31
-rw-r--r--app/helpers/wiki_helper.rb24
-rw-r--r--app/mailers/devise_mailer.rb4
-rw-r--r--app/mailers/emails/groups.rb49
-rw-r--r--app/mailers/emails/profile.rb6
-rw-r--r--app/mailers/emails/projects.rb136
-rw-r--r--app/mailers/notify.rb41
-rw-r--r--app/models/ability.rb34
-rw-r--r--app/models/application_setting.rb65
-rw-r--r--app/models/commit.rb35
-rw-r--r--app/models/commit_range.rb106
-rw-r--r--app/models/concerns/issuable.rb25
-rw-r--r--app/models/concerns/mentionable.rb41
-rw-r--r--app/models/concerns/participable.rb65
-rw-r--r--app/models/concerns/taskable.rb47
-rw-r--r--app/models/deploy_key.rb18
-rw-r--r--app/models/deploy_keys_project.rb10
-rw-r--r--app/models/email.rb5
-rw-r--r--app/models/event.rb26
-rw-r--r--app/models/external_issue.rb4
-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/issue.rb1
-rw-r--r--app/models/key.rb21
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/member.rb142
-rw-r--r--app/models/members/group_member.rb39
-rw-r--r--app/models/members/project_member.rb66
-rw-r--r--app/models/merge_request.rb55
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/namespace.rb57
-rw-r--r--app/models/note.rb108
-rw-r--r--app/models/project.rb66
-rw-r--r--app/models/project_contributions.rb23
-rw-r--r--app/models/project_import_data.rb19
-rw-r--r--app/models/project_services/asana_service.rb4
-rw-r--r--app/models/project_services/bamboo_service.rb6
-rw-r--r--app/models/project_services/buildkite_service.rb (renamed from app/models/project_services/buildbox_service.rb)30
-rw-r--r--app/models/project_services/campfire_service.rb6
-rw-r--r--app/models/project_services/ci_service.rb5
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb1
-rw-r--r--app/models/project_services/emails_on_push_service.rb11
-rw-r--r--app/models/project_services/external_wiki_service.rb54
-rw-r--r--app/models/project_services/flowdock_service.rb1
-rw-r--r--app/models/project_services/gemnasium_service.rb1
-rw-r--r--app/models/project_services/gitlab_ci_service.rb49
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb20
-rw-r--r--app/models/project_services/hipchat_service.rb31
-rw-r--r--app/models/project_services/irker_service.rb13
-rw-r--r--app/models/project_services/issue_tracker_service.rb28
-rw-r--r--app/models/project_services/jira_service.rb11
-rw-r--r--app/models/project_services/pushover_service.rb6
-rw-r--r--app/models/project_services/slack_service/push_message.rb13
-rw-r--r--app/models/project_services/teamcity_service.rb8
-rw-r--r--app/models/project_team.rb70
-rw-r--r--app/models/project_wiki.rb4
-rw-r--r--app/models/protected_branch.rb2
-rw-r--r--app/models/repository.rb97
-rw-r--r--app/models/service.rb27
-rw-r--r--app/models/snippet.rb7
-rw-r--r--app/models/subscription.rb21
-rw-r--r--app/models/tree.rb54
-rw-r--r--app/models/user.rb125
-rw-r--r--app/models/wiki_page.rb3
-rw-r--r--app/services/archive_repository_service.rb64
-rw-r--r--app/services/base_service.rb15
-rw-r--r--app/services/create_branch_service.rb16
-rw-r--r--app/services/create_snippet_service.rb20
-rw-r--r--app/services/create_tag_service.rb15
-rw-r--r--app/services/delete_branch_service.rb14
-rw-r--r--app/services/delete_tag_service.rb42
-rw-r--r--app/services/event_create_service.rb20
-rw-r--r--app/services/files/create_service.rb6
-rw-r--r--app/services/files/delete_service.rb2
-rw-r--r--app/services/files/update_service.rb2
-rw-r--r--app/services/git_push_service.rb91
-rw-r--r--app/services/git_tag_push_service.rb26
-rw-r--r--app/services/issues/bulk_update_service.rb6
-rw-r--r--app/services/issues/update_service.rb3
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/merge_requests/refresh_service.rb18
-rw-r--r--app/services/merge_requests/update_service.rb3
-rw-r--r--app/services/notes/create_service.rb2
-rw-r--r--app/services/notes/update_service.rb3
-rw-r--r--app/services/notification_service.rb129
-rw-r--r--app/services/projects/create_service.rb19
-rw-r--r--app/services/projects/fork_service.rb60
-rw-r--r--app/services/projects/participants_service.rb39
-rw-r--r--app/services/projects/transfer_service.rb3
-rw-r--r--app/services/projects/update_service.rb9
-rw-r--r--app/services/projects/upload_service.rb8
-rw-r--r--app/services/system_hooks_service.rb2
-rw-r--r--app/services/update_snippet_service.rb22
-rw-r--r--app/views/admin/application_settings/_form.html.haml60
-rw-r--r--app/views/admin/application_settings/show.html.haml1
-rw-r--r--app/views/admin/applications/_delete_form.html.haml2
-rw-r--r--app/views/admin/applications/_form.html.haml10
-rw-r--r--app/views/admin/applications/edit.html.haml3
-rw-r--r--app/views/admin/applications/index.html.haml1
-rw-r--r--app/views/admin/applications/new.html.haml3
-rw-r--r--app/views/admin/applications/show.html.haml1
-rw-r--r--app/views/admin/background_jobs/show.html.haml3
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml9
-rw-r--r--app/views/admin/deploy_keys/index.html.haml28
-rw-r--r--app/views/admin/deploy_keys/new.html.haml27
-rw-r--r--app/views/admin/deploy_keys/show.html.haml35
-rw-r--r--app/views/admin/groups/edit.html.haml1
-rw-r--r--app/views/admin/groups/index.html.haml5
-rw-r--r--app/views/admin/groups/new.html.haml1
-rw-r--r--app/views/admin/groups/show.html.haml20
-rw-r--r--app/views/admin/hooks/index.html.haml5
-rw-r--r--app/views/admin/keys/show.html.haml1
-rw-r--r--app/views/admin/logs/show.html.haml1
-rw-r--r--app/views/admin/projects/index.html.haml9
-rw-r--r--app/views/admin/projects/show.html.haml27
-rw-r--r--app/views/admin/services/_form.html.haml77
-rw-r--r--app/views/admin/services/edit.html.haml1
-rw-r--r--app/views/admin/services/index.html.haml1
-rw-r--r--app/views/admin/users/edit.html.haml1
-rw-r--r--app/views/admin/users/index.html.haml13
-rw-r--r--app/views/admin/users/new.html.haml1
-rw-r--r--app/views/admin/users/show.html.haml28
-rw-r--r--app/views/dashboard/_activities.html.haml14
-rw-r--r--app/views/dashboard/_groups.html.haml21
-rw-r--r--app/views/dashboard/_projects.html.haml6
-rw-r--r--app/views/dashboard/_projects_filter.html.haml100
-rw-r--r--app/views/dashboard/_sidebar.html.haml17
-rw-r--r--app/views/dashboard/_zero_authorized_projects.html.haml5
-rw-r--r--app/views/dashboard/groups/index.html.haml20
-rw-r--r--app/views/dashboard/issues.atom.builder6
-rw-r--r--app/views/dashboard/issues.html.haml11
-rw-r--r--app/views/dashboard/merge_requests.html.haml1
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml20
-rw-r--r--app/views/dashboard/milestones/index.html.haml21
-rw-r--r--app/views/dashboard/milestones/show.html.haml1
-rw-r--r--app/views/dashboard/projects.html.haml60
-rw-r--r--app/views/dashboard/projects/starred.html.haml14
-rw-r--r--app/views/dashboard/show.atom.builder6
-rw-r--r--app/views/dashboard/show.html.haml10
-rw-r--r--app/views/devise/mailer/confirmation_instructions.html.erb2
-rw-r--r--app/views/devise/mailer/reset_password_instructions.html.erb2
-rw-r--r--app/views/devise/mailer/unlock_instructions.html.erb2
-rw-r--r--app/views/devise/passwords/edit.html.haml2
-rw-r--r--app/views/devise/registrations/edit.html.erb4
-rw-r--r--app/views/devise/registrations/new.html.haml3
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml6
-rw-r--r--app/views/devise/sessions/new.html.haml1
-rw-r--r--app/views/devise/shared/_omniauth_box.html.haml4
-rw-r--r--app/views/devise/shared/_signin_box.html.haml2
-rw-r--r--app/views/devise/shared/_signup_box.html.haml5
-rw-r--r--app/views/doorkeeper/applications/_delete_form.html.haml2
-rw-r--r--app/views/doorkeeper/applications/edit.html.haml3
-rw-r--r--app/views/doorkeeper/applications/index.html.haml3
-rw-r--r--app/views/doorkeeper/applications/show.html.haml1
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml2
-rw-r--r--app/views/errors/access_denied.html.haml1
-rw-r--r--app/views/errors/encoding.html.haml1
-rw-r--r--app/views/errors/git_not_found.html.haml1
-rw-r--r--app/views/errors/not_found.html.haml1
-rw-r--r--app/views/errors/omniauth_error.html.haml1
-rw-r--r--app/views/events/_event_issue.atom.haml2
-rw-r--r--app/views/events/_event_last_push.html.haml2
-rw-r--r--app/views/events/_event_merge_request.atom.haml2
-rw-r--r--app/views/events/_event_note.atom.haml2
-rw-r--r--app/views/events/_event_push.atom.haml2
-rw-r--r--app/views/events/event/_created_project.html.haml4
-rw-r--r--app/views/events/event/_push.html.haml10
-rw-r--r--app/views/explore/groups/index.html.haml1
-rw-r--r--app/views/explore/projects/_filter.html.haml67
-rw-r--r--app/views/explore/projects/index.html.haml28
-rw-r--r--app/views/explore/projects/starred.html.haml1
-rw-r--r--app/views/explore/projects/trending.html.haml9
-rw-r--r--app/views/groups/_projects.html.haml6
-rw-r--r--app/views/groups/_settings_nav.html.haml11
-rw-r--r--app/views/groups/edit.html.haml5
-rw-r--r--app/views/groups/group_members/_group_member.html.haml52
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml (renamed from app/views/groups/_new_group_member.html.haml)9
-rw-r--r--app/views/groups/group_members/index.html.haml (renamed from app/views/groups/members.html.haml)7
-rw-r--r--app/views/groups/issues.atom.builder6
-rw-r--r--app/views/groups/issues.html.haml11
-rw-r--r--app/views/groups/merge_requests.html.haml1
-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.haml27
-rw-r--r--app/views/groups/milestones/show.html.haml7
-rw-r--r--app/views/groups/new.html.haml2
-rw-r--r--app/views/groups/projects.html.haml9
-rw-r--r--app/views/groups/show.atom.builder8
-rw-r--r--app/views/groups/show.html.haml43
-rw-r--r--app/views/help/_shortcuts.html.haml10
-rw-r--r--app/views/help/show.html.haml3
-rw-r--r--app/views/help/ui.html.haml22
-rw-r--r--app/views/import/base/create.js.haml2
-rw-r--r--app/views/import/bitbucket/status.html.haml6
-rw-r--r--app/views/import/github/status.html.haml6
-rw-r--r--app/views/import/gitlab/status.html.haml6
-rw-r--r--app/views/import/gitorious/status.html.haml6
-rw-r--r--app/views/import/google_code/new.html.haml61
-rw-r--r--app/views/import/google_code/new_user_map.html.haml43
-rw-r--r--app/views/import/google_code/status.html.haml70
-rw-r--r--app/views/invites/show.html.haml30
-rw-r--r--app/views/layouts/_bootlint.haml4
-rw-r--r--app/views/layouts/_empty_head_panel.html.haml4
-rw-r--r--app/views/layouts/_head.html.haml20
-rw-r--r--app/views/layouts/_head_panel.html.haml84
-rw-r--r--app/views/layouts/_page.html.haml36
-rw-r--r--app/views/layouts/_public_head_panel.html.haml34
-rw-r--r--app/views/layouts/admin.html.haml11
-rw-r--r--app/views/layouts/application.html.haml12
-rw-r--r--app/views/layouts/dashboard.html.haml5
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/layouts/doorkeeper/admin.html.haml22
-rw-r--r--app/views/layouts/doorkeeper/application.html.haml15
-rw-r--r--app/views/layouts/errors.html.haml2
-rw-r--r--app/views/layouts/explore.html.haml33
-rw-r--r--app/views/layouts/group.html.haml11
-rw-r--r--app/views/layouts/help.html.haml4
-rw-r--r--app/views/layouts/nav/_admin.html.haml48
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml36
-rw-r--r--app/views/layouts/nav/_explore.html.haml18
-rw-r--r--app/views/layouts/nav/_group.html.haml43
-rw-r--r--app/views/layouts/nav/_profile.html.haml36
-rw-r--r--app/views/layouts/nav/_project.html.haml142
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml41
-rw-r--r--app/views/layouts/nav/_snippets.html.haml12
-rw-r--r--app/views/layouts/navless.html.haml10
-rw-r--r--app/views/layouts/notify.html.haml3
-rw-r--r--app/views/layouts/profile.html.haml11
-rw-r--r--app/views/layouts/project.html.haml8
-rw-r--r--app/views/layouts/project_settings.html.haml12
-rw-r--r--app/views/layouts/projects.html.haml7
-rw-r--r--app/views/layouts/public_group.html.haml6
-rw-r--r--app/views/layouts/public_projects.html.haml6
-rw-r--r--app/views/layouts/public_users.html.haml6
-rw-r--r--app/views/layouts/search.html.haml14
-rw-r--r--app/views/layouts/snippets.html.haml5
-rw-r--r--app/views/notify/_note_message.html.haml2
-rw-r--r--app/views/notify/group_access_granted_email.html.haml2
-rw-r--r--app/views/notify/group_access_granted_email.text.erb2
-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/project_was_moved_email.html.haml4
-rw-r--r--app/views/notify/repository_push_email.html.haml112
-rw-r--r--app/views/notify/repository_push_email.text.haml80
-rw-r--r--app/views/profiles/accounts/show.html.haml22
-rw-r--r--app/views/profiles/applications.html.haml3
-rw-r--r--app/views/profiles/design.html.haml12
-rw-r--r--app/views/profiles/emails/index.html.haml19
-rw-r--r--app/views/profiles/history.html.haml3
-rw-r--r--app/views/profiles/keys/_key.html.haml2
-rw-r--r--app/views/profiles/keys/index.html.haml3
-rw-r--r--app/views/profiles/keys/new.html.haml1
-rw-r--r--app/views/profiles/keys/show.html.haml1
-rw-r--r--app/views/profiles/notifications/show.html.haml7
-rw-r--r--app/views/profiles/passwords/edit.html.haml1
-rw-r--r--app/views/profiles/passwords/new.html.haml2
-rw-r--r--app/views/profiles/show.html.haml19
-rw-r--r--app/views/profiles/update.js.erb8
-rw-r--r--app/views/projects/_aside.html.haml83
-rw-r--r--app/views/projects/_commit_button.html.haml3
-rw-r--r--app/views/projects/_dropdown.html.haml20
-rw-r--r--app/views/projects/_home_panel.html.haml47
-rw-r--r--app/views/projects/_issuable_form.html.haml8
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/_section.html.haml35
-rw-r--r--app/views/projects/_settings_nav.html.haml31
-rw-r--r--app/views/projects/_visibility_level.html.haml27
-rw-r--r--app/views/projects/blame/show.html.haml12
-rw-r--r--app/views/projects/blob/_actions.html.haml12
-rw-r--r--app/views/projects/blob/_blob.html.haml12
-rw-r--r--app/views/projects/blob/edit.html.haml1
-rw-r--r--app/views/projects/blob/new.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml1
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/branches/index.html.haml1
-rw-r--r--app/views/projects/branches/new.html.haml1
-rw-r--r--app/views/projects/commit/_commit_box.html.haml6
-rw-r--r--app/views/projects/commit/show.html.haml1
-rw-r--r--app/views/projects/commits/_commit.html.haml4
-rw-r--r--app/views/projects/commits/_commit_list.html.haml4
-rw-r--r--app/views/projects/commits/_head.html.haml4
-rw-r--r--app/views/projects/commits/show.atom.builder14
-rw-r--r--app/views/projects/commits/show.html.haml5
-rw-r--r--app/views/projects/compare/index.html.haml1
-rw-r--r--app/views/projects/compare/show.html.haml1
-rw-r--r--app/views/projects/deploy_keys/_deploy_key.html.haml31
-rw-r--r--app/views/projects/deploy_keys/index.html.haml26
-rw-r--r--app/views/projects/deploy_keys/new.html.haml1
-rw-r--r--app/views/projects/deploy_keys/show.html.haml1
-rw-r--r--app/views/projects/diffs/_diffs.html.haml20
-rw-r--r--app/views/projects/diffs/_file.html.haml27
-rw-r--r--app/views/projects/diffs/_stats.html.haml7
-rw-r--r--app/views/projects/diffs/_text_file.html.haml4
-rw-r--r--app/views/projects/diffs/_warning.html.haml12
-rw-r--r--app/views/projects/edit.html.haml19
-rw-r--r--app/views/projects/empty.html.haml13
-rw-r--r--app/views/projects/forks/error.html.haml1
-rw-r--r--app/views/projects/forks/new.html.haml1
-rw-r--r--app/views/projects/graphs/commits.html.haml7
-rw-r--r--app/views/projects/graphs/show.html.haml1
-rw-r--r--app/views/projects/hooks/index.html.haml5
-rw-r--r--app/views/projects/imports/new.html.haml3
-rw-r--r--app/views/projects/imports/show.html.haml1
-rw-r--r--app/views/projects/issues/_discussion.html.haml29
-rw-r--r--app/views/projects/issues/_issue.html.haml38
-rw-r--r--app/views/projects/issues/_issue_context.html.haml22
-rw-r--r--app/views/projects/issues/edit.html.haml1
-rw-r--r--app/views/projects/issues/index.atom.builder4
-rw-r--r--app/views/projects/issues/index.html.haml24
-rw-r--r--app/views/projects/issues/new.html.haml1
-rw-r--r--app/views/projects/issues/show.html.haml21
-rw-r--r--app/views/projects/issues/update.js.haml2
-rw-r--r--app/views/projects/labels/_form.html.haml4
-rw-r--r--app/views/projects/labels/edit.html.haml1
-rw-r--r--app/views/projects/labels/index.html.haml1
-rw-r--r--app/views/projects/labels/new.html.haml1
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml14
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml31
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml13
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml7
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml83
-rw-r--r--app/views/projects/merge_requests/_show.html.haml11
-rw-r--r--app/views/projects/merge_requests/edit.html.haml1
-rw-r--r--app/views/projects/merge_requests/index.html.haml32
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml1
-rw-r--r--app/views/projects/merge_requests/new.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_context.html.haml26
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_mr_accept.html.haml22
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_mr_ci.html.haml6
-rw-r--r--app/views/projects/merge_requests/show/_participants.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_remove_source_branch.html.haml2
-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.haml26
-rw-r--r--app/views/projects/milestones/edit.html.haml1
-rw-r--r--app/views/projects/milestones/index.html.haml1
-rw-r--r--app/views/projects/milestones/new.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml12
-rw-r--r--app/views/projects/network/show.html.haml1
-rw-r--r--app/views/projects/new.html.haml133
-rw-r--r--app/views/projects/notes/_discussion.html.haml10
-rw-r--r--app/views/projects/notes/_edit_form.html.haml13
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml38
-rw-r--r--app/views/projects/notes/discussions/_diff.html.haml14
-rw-r--r--app/views/projects/project_members/_group_members.html.haml16
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml18
-rw-r--r--app/views/projects/project_members/_project_member.html.haml53
-rw-r--r--app/views/projects/project_members/_team.html.haml11
-rw-r--r--app/views/projects/project_members/import.html.haml (renamed from app/views/projects/team_members/import.html.haml)5
-rw-r--r--app/views/projects/project_members/index.html.haml36
-rw-r--r--app/views/projects/project_members/update.js.haml3
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml2
-rw-r--r--app/views/projects/protected_branches/index.html.haml13
-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/services/_form.html.haml96
-rw-r--r--app/views/projects/services/edit.html.haml1
-rw-r--r--app/views/projects/services/index.html.haml1
-rw-r--r--app/views/projects/show.atom.builder12
-rw-r--r--app/views/projects/show.html.haml93
-rw-r--r--app/views/projects/snippets/edit.html.haml3
-rw-r--r--app/views/projects/snippets/index.html.haml1
-rw-r--r--app/views/projects/snippets/new.html.haml3
-rw-r--r--app/views/projects/snippets/show.html.haml11
-rw-r--r--app/views/projects/tags/_tag.html.haml4
-rw-r--r--app/views/projects/tags/index.html.haml1
-rw-r--r--app/views/projects/tags/new.html.haml1
-rw-r--r--app/views/projects/team_members/_form.html.haml29
-rw-r--r--app/views/projects/team_members/_group_members.html.haml14
-rw-r--r--app/views/projects/team_members/_team.html.haml9
-rw-r--r--app/views/projects/team_members/_team_member.html.haml17
-rw-r--r--app/views/projects/team_members/index.html.haml16
-rw-r--r--app/views/projects/team_members/new.html.haml1
-rw-r--r--app/views/projects/team_members/update.js.haml6
-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/projects/tree/show.html.haml7
-rw-r--r--app/views/projects/wikis/edit.html.haml3
-rw-r--r--app/views/projects/wikis/empty.html.haml1
-rw-r--r--app/views/projects/wikis/git_access.html.haml1
-rw-r--r--app/views/projects/wikis/history.html.haml1
-rw-r--r--app/views/projects/wikis/pages.html.haml1
-rw-r--r--app/views/projects/wikis/show.html.haml1
-rw-r--r--app/views/search/_category.html.haml77
-rw-r--r--app/views/search/_filter.html.haml4
-rw-r--r--app/views/search/_form.html.haml12
-rw-r--r--app/views/search/_global_filter.html.haml16
-rw-r--r--app/views/search/_project_filter.html.haml32
-rw-r--r--app/views/search/_results.html.haml39
-rw-r--r--app/views/search/_snippet_filter.html.haml13
-rw-r--r--app/views/search/results/_empty.html.haml6
-rw-r--r--app/views/search/results/_snippet_blob.html.haml6
-rw-r--r--app/views/search/show.html.haml29
-rw-r--r--app/views/shared/_choose_group_avatar_button.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml34
-rw-r--r--app/views/shared/_event_filter.html.haml14
-rw-r--r--app/views/shared/_field.html.haml24
-rw-r--r--app/views/shared/_group_form.html.haml2
-rw-r--r--app/views/shared/_issuable_filter.html.haml143
-rw-r--r--app/views/shared/_issuable_search_form.html.haml9
-rw-r--r--app/views/shared/_project.html.haml2
-rw-r--r--app/views/shared/_service_settings.html.haml75
-rw-r--r--app/views/shared/_show_aside.html.haml2
-rw-r--r--app/views/shared/_visibility_level.html.haml14
-rw-r--r--app/views/shared/_visibility_radios.html.haml14
-rw-r--r--app/views/shared/snippets/_form.html.haml2
-rw-r--r--app/views/shared/snippets/_visibility_level.html.haml27
-rw-r--r--app/views/snippets/current_user_index.html.haml54
-rw-r--r--app/views/snippets/edit.html.haml3
-rw-r--r--app/views/snippets/index.html.haml4
-rw-r--r--app/views/snippets/new.html.haml3
-rw-r--r--app/views/snippets/show.html.haml15
-rw-r--r--app/views/snippets/user_index.html.haml3
-rw-r--r--app/views/users/_groups.html.haml2
-rw-r--r--app/views/users/_profile.html.haml12
-rw-r--r--app/views/users/_projects.html.haml10
-rw-r--r--app/views/users/calendar.html.haml10
-rw-r--r--app/views/users/calendar_activities.html.haml23
-rw-r--r--app/views/users/show.atom.builder4
-rw-r--r--app/views/users/show.html.haml51
-rw-r--r--app/views/votes/_votes_block.html.haml14
-rw-r--r--app/views/votes/_votes_inline.html.haml4
-rw-r--r--app/workers/emails_on_push_worker.rb53
-rw-r--r--app/workers/fork_registration_worker.rb12
-rw-r--r--app/workers/irker_worker.rb9
-rw-r--r--app/workers/post_receive.rb29
-rw-r--r--app/workers/repository_archive_worker.rb43
-rw-r--r--app/workers/repository_import_worker.rb2
-rwxr-xr-xbin/background_jobs2
-rwxr-xr-xbin/guard16
-rw-r--r--config/application.rb2
-rw-r--r--config/gitlab.yml.example65
-rw-r--r--config/initializers/1_settings.rb14
-rw-r--r--config/initializers/2_app.rb5
-rw-r--r--config/initializers/5_backend.rb7
-rw-r--r--config/initializers/6_rack_profiler.rb3
-rw-r--r--config/initializers/8_default_url_options.rb11
-rw-r--r--config/initializers/acts_as_taggable_on_patch.rb131
-rw-r--r--config/initializers/devise.rb9
-rw-r--r--config/initializers/doorkeeper.rb4
-rw-r--r--config/initializers/mime_types.rb2
-rw-r--r--config/initializers/public_key.rb2
-rw-r--r--config/initializers/timeout.rb8
-rw-r--r--config/locales/devise.en.yml6
-rw-r--r--config/routes.rb116
-rw-r--r--config/unicorn.rb.example22
-rw-r--r--db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb5
-rw-r--r--db/migrate/20150306023106_fix_namespace_duplication.rb21
-rw-r--r--db/migrate/20150306023112_add_unique_index_to_namespace.rb9
-rw-r--r--db/migrate/20150313012111_create_subscriptions_table.rb16
-rw-r--r--db/migrate/20150320234437_add_location_to_user.rb5
-rw-r--r--db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb6
-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/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb88
-rw-r--r--db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb7
-rw-r--r--db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb10
-rw-r--r--db/migrate/20150425164647_remove_duplicate_tags.rb16
-rw-r--r--db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb27
-rw-r--r--db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb15
-rw-r--r--db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb10
-rw-r--r--db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb10
-rw-r--r--db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb7
-rw-r--r--db/migrate/20150429002313_remove_abandoned_group_members_records.rb6
-rw-r--r--db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb5
-rw-r--r--db/schema.rb50
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/groups.md2
-rw-r--r--doc/api/issues.md2
-rw-r--r--doc/api/merge_requests.md6
-rw-r--r--doc/api/milestones.md2
-rw-r--r--doc/api/notes.md2
-rw-r--r--doc/api/project_snippets.md15
-rw-r--r--doc/api/projects.md15
-rw-r--r--doc/api/users.md6
-rw-r--r--doc/customization/issue_closing.md37
-rw-r--r--doc/development/architecture.md2
-rw-r--r--doc/install/installation.md18
-rw-r--r--doc/install/requirements.md9
-rw-r--r--doc/integration/bitbucket.md17
-rw-r--r--doc/integration/external-issue-tracker.md5
-rw-r--r--doc/integration/gitlab_buttons_in_gmail.md8
-rw-r--r--doc/integration/ldap.md5
-rw-r--r--doc/integration/slack.md11
-rw-r--r--doc/logs/logs.md16
-rw-r--r--doc/markdown/markdown.md165
-rw-r--r--doc/operations/sidekiq_memory_killer.md2
-rw-r--r--doc/permissions/permissions.md7
-rw-r--r--doc/project_services/hipchat.md54
-rw-r--r--doc/project_services/project_services.md2
-rw-r--r--doc/public_access/public_access.md2
-rw-r--r--doc/raketasks/backup_restore.md8
-rw-r--r--doc/raketasks/user_management.md9
-rw-r--r--doc/release/master.md29
-rw-r--r--doc/release/monthly.md14
-rw-r--r--doc/release/patch.md16
-rw-r--r--doc/release/security.md4
-rw-r--r--doc/ssh/README.md10
-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.8.md)33
-rw-r--r--doc/update/7.6-to-7.7.md2
-rw-r--r--doc/update/7.8-to-7.9.md122
-rw-r--r--doc/update/7.9-to-7.10.md118
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/upgrader.md14
-rw-r--r--doc/web_hooks/web_hooks.md2
-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--doc/workflow/protected_branches.md6
-rw-r--r--docker/README.md149
-rw-r--r--docker/app/Dockerfile (renamed from docker/Dockerfile)8
-rwxr-xr-xdocker/app/assets/wrapper (renamed from docker/assets/wrapper)0
-rw-r--r--docker/data/Dockerfile8
-rw-r--r--docker/data/assets/gitlab.rb (renamed from docker/assets/gitlab.rb)0
-rw-r--r--docker/single/Dockerfile35
-rwxr-xr-xdocker/single/assets/wrapper16
-rw-r--r--docker/single/marathon.json14
-rw-r--r--docker/troubleshooting.md10
-rw-r--r--features/admin/deploy_keys.feature21
-rw-r--r--features/admin/settings.feature10
-rw-r--r--features/dashboard/archived_projects.feature5
-rw-r--r--features/dashboard/group.feature8
-rw-r--r--features/dashboard/issues.feature2
-rw-r--r--features/dashboard/merge_requests.feature2
-rw-r--r--features/dashboard/new_project.feature13
-rw-r--r--features/dashboard/projects.feature9
-rw-r--r--features/groups.feature23
-rw-r--r--features/invites.feature45
-rw-r--r--features/project/archived.feature9
-rw-r--r--features/project/commits/commits.feature3
-rw-r--r--features/project/deploy_keys.feature21
-rw-r--r--features/project/issues/filter_labels.feature6
-rw-r--r--features/project/issues/issues.feature52
-rw-r--r--features/project/merge_requests.feature49
-rw-r--r--features/project/project.feature7
-rw-r--r--features/project/star.feature2
-rw-r--r--features/project/team_management.feature24
-rw-r--r--features/project/wiki.feature24
-rw-r--r--features/search.feature6
-rw-r--r--features/steps/admin/deploy_keys.rb57
-rw-r--r--features/steps/admin/groups.rb2
-rw-r--r--features/steps/admin/settings.rb40
-rw-r--r--features/steps/dashboard/group.rb19
-rw-r--r--features/steps/dashboard/issues.rb17
-rw-r--r--features/steps/dashboard/merge_requests.rb17
-rw-r--r--features/steps/dashboard/new_project.rb29
-rw-r--r--features/steps/dashboard/projects.rb11
-rw-r--r--features/steps/explore/groups.rb2
-rw-r--r--features/steps/groups.rb62
-rw-r--r--features/steps/invites.rb80
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/project/commits/commits.rb14
-rw-r--r--features/steps/project/deploy_keys.rb26
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/issues/filter_labels.rb23
-rw-r--r--features/steps/project/issues/issues.rb39
-rw-r--r--features/steps/project/merge_requests.rb40
-rw-r--r--features/steps/project/project.rb10
-rw-r--r--features/steps/project/source/browse_files.rb2
-rw-r--r--features/steps/project/source/search_code.rb2
-rw-r--r--features/steps/project/star.rb8
-rw-r--r--features/steps/project/team_management.rb67
-rw-r--r--features/steps/project/wiki.rb41
-rw-r--r--features/steps/search.rb20
-rw-r--r--features/steps/shared/active_tab.rb2
-rw-r--r--features/steps/shared/markdown.rb51
-rw-r--r--features/steps/shared/note.rb14
-rw-r--r--features/steps/shared/paths.rb26
-rw-r--r--features/steps/shared/project.rb7
-rw-r--r--features/steps/snippets/user.rb6
-rw-r--r--features/steps/user.rb33
-rw-r--r--features/support/capybara.rb24
-rw-r--r--features/support/db_cleaner.rb11
-rw-r--r--features/support/env.rb30
-rw-r--r--features/user.feature9
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/branches.rb3
-rw-r--r--lib/api/commits.rb8
-rw-r--r--lib/api/entities.rb9
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/group_members.rb21
-rw-r--r--lib/api/groups.rb6
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/internal.rb38
-rw-r--r--lib/api/issues.rb3
-rw-r--r--lib/api/merge_requests.rb23
-rw-r--r--lib/api/project_members.rb28
-rw-r--r--lib/api/project_snippets.rb26
-rw-r--r--lib/api/projects.rb13
-rw-r--r--lib/api/repositories.rb13
-rw-r--r--lib/api/users.rb4
-rw-r--r--lib/backup/manager.rb94
-rw-r--r--lib/backup/repository.rb5
-rw-r--r--lib/file_size_validator.rb12
-rw-r--r--lib/gitlab.rb5
-rw-r--r--lib/gitlab/backend/grack_auth.rb47
-rw-r--r--lib/gitlab/backend/rack_attack_helpers.rb31
-rw-r--r--lib/gitlab/backend/shell.rb2
-rw-r--r--lib/gitlab/bitbucket_import/client.rb4
-rw-r--r--lib/gitlab/bitbucket_import/project_creator.rb19
-rw-r--r--lib/gitlab/closing_issue_extractor.rb23
-rw-r--r--lib/gitlab/commits_calendar.rb33
-rw-r--r--lib/gitlab/contributions_calendar.rb56
-rw-r--r--lib/gitlab/contributor.rb (renamed from lib/gitlab/contributors.rb)0
-rw-r--r--lib/gitlab/current_settings.rb5
-rw-r--r--lib/gitlab/force_push_check.rb7
-rw-r--r--lib/gitlab/git.rb20
-rw-r--r--lib/gitlab/git_access.rb140
-rw-r--r--lib/gitlab/git_access_wiki.rb2
-rw-r--r--lib/gitlab/github_import/client.rb2
-rw-r--r--lib/gitlab/github_import/project_creator.rb19
-rw-r--r--lib/gitlab/gitlab_import/client.rb6
-rw-r--r--lib/gitlab/gitlab_import/project_creator.rb19
-rw-r--r--lib/gitlab/gitorious_import/client.rb34
-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.rb52
-rw-r--r--lib/gitlab/google_code_import/importer.rb377
-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/identifier.rb2
-rw-r--r--lib/gitlab/key_fingerprint.rb55
-rw-r--r--lib/gitlab/ldap/access.rb10
-rw-r--r--lib/gitlab/ldap/authentication.rb4
-rw-r--r--lib/gitlab/ldap/config.rb6
-rw-r--r--lib/gitlab/ldap/person.rb1
-rw-r--r--lib/gitlab/ldap/user.rb15
-rw-r--r--lib/gitlab/markdown.rb385
-rw-r--r--lib/gitlab/markdown/autolink_filter.rb100
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb84
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb82
-rw-r--r--lib/gitlab/markdown/cross_project_reference.rb32
-rw-r--r--lib/gitlab/markdown/emoji_filter.rb79
-rw-r--r--lib/gitlab/markdown/external_issue_reference_filter.rb63
-rw-r--r--lib/gitlab/markdown/issue_reference_filter.rb72
-rw-r--r--lib/gitlab/markdown/label_reference_filter.rb96
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb74
-rw-r--r--lib/gitlab/markdown/reference_filter.rb94
-rw-r--r--lib/gitlab/markdown/sanitization_filter.rb38
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb74
-rw-r--r--lib/gitlab/markdown/table_of_contents_filter.rb62
-rw-r--r--lib/gitlab/markdown/user_reference_filter.rb105
-rw-r--r--lib/gitlab/middleware/timeout.rb13
-rw-r--r--lib/gitlab/note_data_builder.rb4
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb (renamed from lib/gitlab/oauth/auth_hash.rb)19
-rw-r--r--lib/gitlab/o_auth/user.rb (renamed from lib/gitlab/oauth/user.rb)2
-rw-r--r--lib/gitlab/popen.rb2
-rw-r--r--lib/gitlab/project_search_results.rb2
-rw-r--r--lib/gitlab/push_data_builder.rb26
-rw-r--r--lib/gitlab/reference_extractor.rb109
-rw-r--r--lib/gitlab/regex.rb66
-rw-r--r--lib/gitlab/satellite/merge_action.rb2
-rw-r--r--lib/gitlab/satellite/satellite.rb11
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb11
-rw-r--r--lib/gitlab/theme.rb39
-rw-r--r--lib/gitlab/url_builder.rb6
-rw-r--r--lib/gitlab/utils.rb4
-rw-r--r--lib/gitlab/visibility_level.rb26
-rw-r--r--lib/redcarpet/render/gitlab_html.rb44
-rwxr-xr-xlib/support/deploy/deploy.sh2
-rw-r--r--lib/tasks/brakeman.rake2
-rw-r--r--lib/tasks/dev.rake5
-rw-r--r--lib/tasks/gitlab/backup.rake33
-rw-r--r--lib/tasks/gitlab/check.rake50
-rw-r--r--lib/tasks/gitlab/cleanup.rake9
-rw-r--r--lib/tasks/gitlab/shell.rake1
-rw-r--r--lib/tasks/gitlab/task_helpers.rake16
-rw-r--r--lib/tasks/gitlab/test.rake1
-rw-r--r--lib/tasks/jasmine.rake12
-rw-r--r--public/503.html13
-rw-r--r--public/deploy.html4
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb51
-rw-r--r--spec/controllers/commit_controller_spec.rb2
-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.rb103
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb106
-rw-r--r--spec/controllers/import/google_code_controller_spec.rb60
-rw-r--r--spec/controllers/merge_requests_controller_spec.rb20
-rw-r--r--spec/controllers/namespaces_controller_spec.rb121
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb22
-rw-r--r--spec/controllers/projects/refs_controller_spec.rb41
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb65
-rw-r--r--spec/controllers/projects/uploads_controller_spec.rb223
-rw-r--r--spec/controllers/uploads_controller_spec.rb296
-rw-r--r--spec/controllers/users_controller_spec.rb35
-rw-r--r--spec/factories.rb12
-rw-r--r--spec/factories/projects.rb28
-rw-r--r--spec/factories_spec.rb6
-rw-r--r--spec/features/atom/users_spec.rb27
-rw-r--r--spec/features/gitlab_flavored_markdown_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb2
-rw-r--r--spec/features/issues_spec.rb8
-rw-r--r--spec/features/markdown_spec.rb413
-rw-r--r--spec/features/security/dashboard_access_spec.rb4
-rw-r--r--spec/features/security/group/group_access_spec.rb4
-rw-r--r--spec/features/security/group/internal_group_access_spec.rb4
-rw-r--r--spec/features/security/group/mixed_group_access_spec.rb4
-rw-r--r--spec/features/security/group/public_group_access_spec.rb4
-rw-r--r--spec/features/security/project/internal_access_spec.rb4
-rw-r--r--spec/features/security/project/private_access_spec.rb4
-rw-r--r--spec/features/security/project/public_access_spec.rb4
-rw-r--r--spec/features/task_lists_spec.rb151
-rw-r--r--spec/features/users_spec.rb36
-rw-r--r--spec/finders/issues_finder_spec.rb2
-rw-r--r--spec/fixtures/GoogleCodeProjectHosting.json407
-rw-r--r--spec/fixtures/markdown.md.erb196
-rw-r--r--spec/helpers/application_helper_spec.rb54
-rw-r--r--spec/helpers/diff_helper_spec.rb56
-rw-r--r--spec/helpers/events_helper_spec.rb12
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb848
-rw-r--r--spec/helpers/groups_helper.rb21
-rw-r--r--spec/helpers/icons_helper_spec.rb109
-rw-r--r--spec/helpers/issues_helper_spec.rb18
-rw-r--r--spec/helpers/labels_helper_spec.rb4
-rw-r--r--spec/helpers/submodule_helper_spec.rb42
-rw-r--r--spec/helpers/tree_helper_spec.rb2
-rw-r--r--spec/helpers/visibility_level_helper_spec.rb75
-rw-r--r--spec/javascripts/helpers/.gitkeep0
-rw-r--r--spec/javascripts/issue_spec.js.coffee36
-rw-r--r--spec/javascripts/merge_request_spec.js.coffee36
-rw-r--r--spec/javascripts/notes_spec.js.coffee30
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js.coffee83
-rw-r--r--spec/javascripts/stat_graph_contributors_graph_spec.js2
-rw-r--r--spec/javascripts/stat_graph_contributors_util_spec.js2
-rw-r--r--spec/javascripts/stat_graph_spec.js2
-rw-r--r--spec/javascripts/support/jasmine.yml83
-rw-r--r--spec/javascripts/support/jasmine_helper.rb6
-rw-r--r--spec/lib/extracts_path_spec.rb16
-rw-r--r--spec/lib/file_size_validator_spec.rb43
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb52
-rw-r--r--spec/lib/gitlab/backend/rack_attack_helpers_spec.rb35
-rw-r--r--spec/lib/gitlab/bitbucket_import/client_spec.rb17
-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/diff/file_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/parser_spec.rb2
-rw-r--r--spec/lib/gitlab/git_access_spec.rb38
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb16
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb6
-rw-r--r--spec/lib/gitlab/gitlab_import/client_spec.rb16
-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.rb35
-rw-r--r--spec/lib/gitlab/google_code_import/importer_spec.rb85
-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/access_spec.rb18
-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/markdown/autolink_filter_spec.rb106
-rw-r--r--spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb144
-rw-r--r--spec/lib/gitlab/markdown/commit_reference_filter_spec.rb138
-rw-r--r--spec/lib/gitlab/markdown/cross_project_reference_spec.rb56
-rw-r--r--spec/lib/gitlab/markdown/emoji_filter_spec.rb97
-rw-r--r--spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb92
-rw-r--r--spec/lib/gitlab/markdown/issue_reference_filter_spec.rb143
-rw-r--r--spec/lib/gitlab/markdown/label_reference_filter_spec.rb153
-rw-r--r--spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb124
-rw-r--r--spec/lib/gitlab/markdown/sanitization_filter_spec.rb81
-rw-r--r--spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb122
-rw-r--r--spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb101
-rw-r--r--spec/lib/gitlab/markdown/user_reference_filter_spec.rb134
-rw-r--r--spec/lib/gitlab/o_auth/auth_hash_spec.rb110
-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/oauth/auth_hash_spec.rb55
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb152
-rw-r--r--spec/lib/gitlab/regex_spec.rb19
-rw-r--r--spec/mailers/notify_spec.rb131
-rw-r--r--spec/models/application_setting_spec.rb51
-rw-r--r--spec/models/commit_range_spec.rb120
-rw-r--r--spec/models/commit_spec.rb13
-rw-r--r--spec/models/deploy_key_spec.rb1
-rw-r--r--spec/models/deploy_keys_project_spec.rb48
-rw-r--r--spec/models/external_wiki_service_spec.rb59
-rw-r--r--spec/models/issue_spec.rb3
-rw-r--r--spec/models/key_spec.rb14
-rw-r--r--spec/models/member_spec.rb167
-rw-r--r--spec/models/members/group_member_spec.rb18
-rw-r--r--spec/models/members/project_member_spec.rb6
-rw-r--r--spec/models/members_spec.rb20
-rw-r--r--spec/models/merge_request_spec.rb31
-rw-r--r--spec/models/namespace_spec.rb12
-rw-r--r--spec/models/note_spec.rb182
-rw-r--r--spec/models/project_services/asana_service_spec.rb (renamed from spec/models/asana_service_spec.rb)1
-rw-r--r--spec/models/project_services/assembla_service_spec.rb1
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb (renamed from spec/models/project_services/buildbox_service_spec.rb)19
-rw-r--r--spec/models/project_services/flowdock_service_spec.rb1
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb1
-rw-r--r--spec/models/project_services/gitlab_ci_service_spec.rb28
-rw-r--r--spec/models/project_services/gitlab_issue_tracker_service_spec.rb18
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb22
-rw-r--r--spec/models/project_services/irker_service_spec.rb1
-rw-r--r--spec/models/project_services/jira_service_spec.rb1
-rw-r--r--spec/models/project_services/pushover_service_spec.rb1
-rw-r--r--spec/models/project_services/slack_service/push_message_spec.rb6
-rw-r--r--spec/models/project_services/slack_service_spec.rb1
-rw-r--r--spec/models/project_spec.rb61
-rw-r--r--spec/models/repository_spec.rb7
-rw-r--r--spec/models/service_spec.rb1
-rw-r--r--spec/models/user_spec.rb59
-rw-r--r--spec/models/wiki_page_spec.rb41
-rw-r--r--spec/requests/api/fork_spec.rb1
-rw-r--r--spec/requests/api/groups_spec.rb19
-rw-r--r--spec/requests/api/issues_spec.rb8
-rw-r--r--spec/requests/api/merge_requests_spec.rb15
-rw-r--r--spec/requests/api/milestones_spec.rb7
-rw-r--r--spec/requests/api/projects_spec.rb42
-rw-r--r--spec/requests/api/repositories_spec.rb2
-rw-r--r--spec/requests/api/users_spec.rb17
-rw-r--r--spec/routing/project_routing_spec.rb20
-rw-r--r--spec/routing/routing_spec.rb55
-rw-r--r--spec/services/archive_repository_service_spec.rb93
-rw-r--r--spec/services/create_snippet_service_spec.rb44
-rw-r--r--spec/services/git_push_service_spec.rb36
-rw-r--r--spec/services/git_tag_push_service_spec.rb52
-rw-r--r--spec/services/notification_service_spec.rb44
-rw-r--r--spec/services/projects/create_service_spec.rb27
-rw-r--r--spec/services/projects/fork_service_spec.rb26
-rw-r--r--spec/services/projects/update_service_spec.rb6
-rw-r--r--spec/services/projects/upload_service_spec.rb10
-rw-r--r--spec/services/update_snippet_service_spec.rb52
-rw-r--r--spec/spec_helper.rb10
-rw-r--r--spec/support/capybara.rb21
-rw-r--r--spec/support/mentionable_shared_examples.rb120
-rw-r--r--spec/support/reference_filter_spec_helper.rb50
-rw-r--r--spec/support/repo_helpers.rb19
-rw-r--r--spec/support/select2_helper.rb4
-rw-r--r--spec/support/taskable_shared_examples.rb36
-rw-r--r--spec/support/test_env.rb60
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb114
-rw-r--r--spec/workers/fork_registration_worker_spec.rb10
-rw-r--r--spec/workers/post_receive_spec.rb18
-rw-r--r--spec/workers/repository_archive_worker_spec.rb80
-rw-r--r--vendor/assets/javascripts/chart-lib.min.js8
-rwxr-xr-xvendor/assets/javascripts/jasmine-fixture.js433
1085 files changed, 20410 insertions, 9265 deletions
diff --git a/.gitignore b/.gitignore
index 7a7b5c93936..3e30fb8cf77 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,6 @@ public/assets/
public/uploads.*
public/uploads/
rails_best_practices_output.html
-tags
+/tags
tmp/
vendor/bundle/*
diff --git a/.rubocop.yml b/.rubocop.yml
index 53ca2ca2191..0cc729d3b08 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -355,7 +355,7 @@ Style/MultilineBlockChain:
Style/MultilineBlockLayout:
Description: 'Ensures newlines after multiline block do statements.'
- Enabled: false
+ Enabled: true
Style/MultilineIfThen:
Description: 'Do not use then for multi-line if/unless.'
@@ -390,7 +390,7 @@ Style/NegatedWhile:
Style/NestedTernaryOperator:
Description: 'Use one expression per branch in a ternary operator.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-nested-ternary'
- Enabled: false
+ Enabled: true
Style/Next:
Description: 'Use `next` to skip iteration instead of a condition at the end.'
@@ -400,17 +400,17 @@ Style/Next:
Style/NilComparison:
Description: 'Prefer x.nil? to x == nil.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#predicate-methods'
- Enabled: false
+ Enabled: true
Style/NonNilCheck:
Description: 'Checks for redundant nil checks.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-non-nil-checks'
- Enabled: false
+ Enabled: true
Style/Not:
Description: 'Use ! instead of not.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#bang-not-not'
- Enabled: false
+ Enabled: true
Style/NumericLiterals:
Description: >-
@@ -424,7 +424,7 @@ Style/OneLineConditional:
Favor the ternary operator(?:) over
if/then/else/end constructs.
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#ternary-operator'
- Enabled: false
+ Enabled: true
Style/OpMethod:
Description: 'When defining binary operators, name the argument other.'
@@ -436,7 +436,7 @@ Style/ParenthesesAroundCondition:
Don't use parentheses around the condition of an
if/unless/while.
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-parens-if'
- Enabled: false
+ Enabled: true
Style/PercentLiteralDelimiters:
Description: 'Use `%`-literal delimiters consistently'
@@ -480,7 +480,7 @@ Style/RedundantException:
Style/RedundantReturn:
Description: "Don't use return where it's not required."
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#no-explicit-return'
- Enabled: false
+ Enabled: true
Style/RedundantSelf:
Description: "Don't use self where it's not needed."
@@ -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.'
@@ -954,7 +954,7 @@ Lint/Void:
Rails/ActionFilter:
Description: 'Enforces consistent use of action filter methods.'
- Enabled: false
+ Enabled: true
Rails/DefaultScope:
Description: 'Checks if the argument passed to default_scope is a block.'
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 b210a6b0155..84bdf78e980 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,17 +1,196 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 7.9.0 (unreleased)
+v 7.11.0 (unreleased)
+ - Make the first branch pushed to an empty repository the default HEAD (Stan Hu)
+ - Make Reply-To config apply to change e-mail confirmation and other Devise notifications (Stan Hu)
+ - Add application setting to restrict user signups to e-mail domains (Stan Hu)
+ - Don't allow a merge request to be merged when its title starts with "WIP".
+ - Add a page title to every page.
+ - Allow primary email to be set to an email that you've already added.
+ - Fix Error 500 when searching Wiki pages (Stan Hu)
+ - Get Gitorious importer to work again.
+ - Fix clone URL field and X11 Primary selection (Dmitry Medvinsky)
+ - Ignore invalid lines in .gitmodules
+ - Fix "Cannot move project" error message from popping up after a successful transfer (Stan Hu)
+ - Redirect to sign in page after signing out.
+ - Fix "Hello @username." references not working by no longer allowing usernames to end in period.
+ - Fix "Revspec not found" errors when viewing diffs in a forked project with submodules (Stan Hu)
+ - Improve project page UI
+ - Fix broken file browsing with relative submodule in personal projects (Stan Hu)
+ - Fix DB error when trying to tag a repository (Stan Hu)
+ - Add "Reply quoting selected text" shortcut key (`r`)
+ - Fix bug causing `@whatever` inside an issue's first code block to be picked up as a user mention.
+ - Fix bug causing `@whatever` inside an inline code snippet (backtick-style) to be picked up as a user mention.
+ - When use change branches link at MR form - save source branch selection instead of target one
+ - Improve handling of large diffs
+ -
+ - Show Atom feed buttons everywhere where applicable.
+ - Add project activity atom feed.
+ - Don't crash when an MR from a fork has a cross-reference comment from the target project on one of its commits.
+ - Include commit comments in MR from a forked project.
+ - Fix adding new group members from admin area
+ - Group milestones by title in the dashboard and all other issue views.
+ - Query issues, merge requests and milestones with their IID through API (Julien Bianchi)
+ - Add default project and snippet visibility settings to the admin web UI.
+ - Show incompatible projects in Google Code import status (Stan Hu)
+ - Fix bug where commit data would not appear in some subdirectories (Stan Hu)
+ - Unescape branch names in compare commit (Stan Hu)
+ - Task lists are now usable in comments, and will show up in Markdown previews.
+ - Fix bug where Slack service channel was not saved in admin template settings. (Stan Hu)
+ - Move snippets UI to fluid layout
+ - Improve UI for sidebar. Increase separation between navigation and content
+ - Improve new project command options (Ben Bodenmiller)
+ - Add common method to force UTF-8 and use it to properly handle non-ascii OAuth user properties (Onur Küçük)
+ - Prevent sending empty messages to HipChat (Chulki Lee)
+ - Improve UI for mobile phones on dashboard and project pages
+ - Add room notification and message color option for HipChat
+ - Allow to use non-ASCII letters and dashes in project and namespace name. (Jakub Jirutka)
+ - Add footnotes support to Markdown (Guillaume Delbergue)
+ - Add current_sign_in_at to UserFull REST api.
+ - Make Sidekiq MemoryKiller shutdown signal configurable
+
+v 7.10.2
+ - Fix CI links on MR page
+
+v 7.10.0
+ - Ignore submodules that are defined in .gitmodules but are checked in as directories.
+ - Allow projects to be imported from Google Code.
+ - Remove access control for uploaded images to fix broken images in emails (Hannes Rosenögger)
+ - 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)
+ - Fix "Import projects from" button to show the correct instructions (Stan Hu)
+ - Fix dots in Wiki slugs causing errors (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)
+ - 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 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+)
+ - Add location field to user profile
+ - Fix print view for markdown files and wiki pages
+ - Fix errors when deleting old backups
+ - Improve GitLab performance when working with git repositories
+ - Add tag message and last commit to tag hook (Kamil Trzciński)
+ - Restrict permissions on backup files
+ - Improve oauth accounts UI in profile page
+ - 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 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.
+ - 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)
+ - 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.
+ - 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)
+ - Update documentation for object_kind field in Webhook push and tag push Webhooks (Stan Hu)
+ - Fix broken email images (Hannes Rosenögger)
+ - Automatically config git if user forgot, where possible (Zeger-Jan van de Weg)
- Fix mass SQL statements on initial push (Hannes Rosenögger)
- Add tag push notifications and normalize HipChat and Slack messages to be consistent (Stan Hu)
- Add comment notification events to HipChat and Slack services (Stan Hu)
- Add issue and merge request events to HipChat and Slack services (Stan Hu)
- Fix merge request URL passed to Webhooks. (Stan Hu)
- Fix bug that caused a server error when editing a comment to "+1" or "-1" (Stan Hu)
+ - Fix code preview theme setting for comments, issues, merge requests, and snippets (Stan Hu)
- Move labels/milestones tabs to sidebar
- Upgrade Rails gem to version 4.1.9.
- Improve error messages for file edit failures
- Improve UI for commits, issues and merge request lists
- Fix commit comments on first line of diff not rendering in Merge Request Discussion view.
+ - Allow admins to override restricted project visibility settings.
+ - Move restricted visibility settings from gitlab.yml into the web UI.
- Improve trigger merge request hook when source project branch has been updated (Kirill Zaitsev)
- Save web edit in new branch
- Fix ordering of imported but unchanged projects (Marco Wessel)
@@ -20,6 +199,7 @@ v 7.9.0 (unreleased)
- Fix checkbox alignment on the application settings page.
- Generalize image upload in drag and drop in markdown to all files (Hannes Rosenögger)
- Fix mass-unassignment of issues (Robert Speicher)
+ - Fix hidden diff comments in merge request discussion view
- Allow user confirmation to be skipped for new users via API
- Add a service to send updates to an Irker gateway (Romain Coltel)
- Add brakeman (security scanner for Ruby on Rails)
@@ -33,7 +213,55 @@ v 7.9.0 (unreleased)
- Send notifications and leave system comments when bulk updating issues.
- Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison)
- Move groups page from profile to dashboard
- - Starred projects page at dashboard
+ - Starred projects page at dashboard
+ - Blocking user does not remove him/her from project/groups but show blocked label
+ - Change subject of EmailsOnPush emails to include namespace, project and branch.
+ - Change subject of EmailsOnPush emails to include first commit message when multiple were pushed.
+ - Remove confusing footer from EmailsOnPush mail body.
+ - Add list of changed files to EmailsOnPush emails.
+ - Add option to send EmailsOnPush emails from committer email if domain matches.
+ - Add option to disable code diffs in EmailOnPush emails.
+ - Wrap commit message in EmailsOnPush email.
+ - Send EmailsOnPush emails when deleting commits using force push.
+ - Fix EmailsOnPush email comparison link to include first commit.
+ - Fix highliht of selected lines in file
+ - Reject access to group/project avatar if the user doesn't have access.
+ - Add database migration to clean group duplicates with same path and name (Make sure you have a backup before update)
+ - Add GitLab active users count to rake gitlab:check
+ - Starred projects page at dashboard
+ - Make email display name configurable
+ - Improve json validation in hook data
+ - Use Emoji One
+ - Updated emoji help documentation to properly reference EmojiOne.
+ - Fix missing GitHub organisation repositories on import page.
+ - Added blue theme
+ - Remove annoying notice messages when create/update merge request
+ - Allow smb:// links in Markdown text.
+ - Filter merge request by title or description at Merge Requests page
+ - Block user if he/she was blocked in Active Directory
+ - Fix import pages not working after first load.
+ - Use custom LDAP label in LDAP signin form.
+ - Execute hooks and services when branch or tag is created or deleted through web interface.
+ - Block and unblock user if he/she was blocked/unblocked in Active Directory
+ - Raise recommended number of unicorn workers from 2 to 3
+ - Use same layout and interactivity for project members as group members.
+ - Prevent gitlab-shell character encoding issues by receiving its changes as raw data.
+ - Ability to unsubscribe/subscribe to issue or merge request
+ - Delete deploy key when last connection to a project is destroyed.
+ - 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
+ - Fix path and name duplication in namespaces
+
+v 7.8.3
+ - Bump version of gitlab_git fixing annotated tags without message
v 7.8.2
- Fix service migration issue when upgrading from versions prior to 7.3
@@ -44,6 +272,7 @@ v 7.8.2
- Fix import check for case sensetive namespaces
- Increase timeout for Git-over-HTTP requests to 1 hour since large pulls/pushes can take a long time.
- Properly handle autosave local storage exceptions.
+ - Escape wildcards when searching LDAP by username.
v 7.8.1
- Fix run of custom post receive hooks
@@ -64,7 +293,6 @@ v 7.8.0
- Add API endpoint to fetch all changes on a MergeRequest (Jeroen van Baarsen)
- View note image attachments in new tab when clicked instead of downloading them
- Improve sorting logic in UI and API. Explicitly define what sorting method is used by default
- - Allow more variations for commit messages closing issues (Julien Bianchi and Hannes Rosenögger)
- Fix overflow at sidebar when have several items
- Add notes for label changes in issue and merge requests
- Show tags in commit view (Hannes Rosenögger)
@@ -86,7 +314,7 @@ v 7.8.0
- Add a commit calendar to the user profile (Hannes Rosenögger)
- Submit comment on command-enter
- Notify all members of a group when that group is mentioned in a comment, for example: `@gitlab-org` or `@sales`.
- - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close"
+ - Extend issue clossing pattern to include "Resolve", "Resolves", "Resolved", "Resolving" and "Close" (Julien Bianchi and Hannes Rosenögger)
- Fix long broadcast message cut-off on left sidebar (Visay Keo)
- Add Project Avatars (Steven Thonus and Hannes Rosenögger)
- Password reset token validity increased from 2 hours to 2 days since it is also send on account creation.
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 fe16b348d97..097a15a2af3 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.5.4
+2.6.2
diff --git a/Gemfile b/Gemfile
index 462c932584d..f950c5be154 100644
--- a/Gemfile
+++ b/Gemfile
@@ -31,7 +31,7 @@ gem 'omniauth-shibboleth'
gem 'omniauth-kerberos'
gem 'omniauth-gitlab'
gem 'omniauth-bitbucket'
-gem 'doorkeeper', '2.1.0'
+gem 'doorkeeper', '2.1.3'
gem "rack-oauth2", "~> 1.0.5"
# Browser detection
@@ -39,16 +39,16 @@ gem "browser"
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '7.0.1'
+gem "gitlab_git", '~> 7.1.10'
# Ruby/Rack Git Smart-HTTP Server Handler
-gem 'gitlab-grack', '~> 2.0.0.rc2', require: 'grack'
+gem 'gitlab-grack', '~> 2.0.2', require: 'grack'
# LDAP Auth
-gem 'gitlab_omniauth-ldap', '1.2.0', require: "omniauth-ldap"
+gem 'gitlab_omniauth-ldap', '1.2.1', require: "omniauth-ldap"
# Git Wiki
-gem 'gollum-lib', '~> 4.0.0'
+gem 'gollum-lib', '~> 4.0.2'
# Language detection
gem "gitlab-linguist", "~> 3.0.1", require: "linguist"
@@ -87,20 +87,17 @@ gem "six"
# Seed data
gem "seed-fu"
-# Markup pipeline for GitLab
-gem 'html-pipeline-gitlab', '~> 0.1.0'
-
-# Markdown to HTML
-gem "github-markup"
-
-# Required markup gems by github-markdown
-gem 'redcarpet', '~> 3.1.2'
+# Markdown and HTML processing
+gem 'html-pipeline', '~> 1.11.0'
+gem 'task_list', '~> 1.0.0', require: 'task_list/railtie'
+gem 'github-markup'
+gem 'redcarpet', '~> 3.2.3'
gem 'RedCloth'
-gem 'rdoc', '~>3.6'
-gem 'org-ruby', '= 0.9.12'
-gem 'creole', '~>0.3.6'
-gem 'wikicloth', '=0.8.1'
-gem 'asciidoctor', '= 0.1.4'
+gem 'rdoc', '~>3.6'
+gem 'org-ruby', '= 0.9.12'
+gem 'creole', '~>0.3.6'
+gem 'wikicloth', '=0.8.1'
+gem 'asciidoctor', '= 0.1.4'
# Diffs
gem 'diffy', '~> 3.0.3'
@@ -115,12 +112,13 @@ end
gem "state_machine"
# Issue tags
-gem "acts-as-taggable-on"
+gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
gem 'slim'
gem 'sinatra', require: nil
gem 'sidekiq', '~> 3.3'
+gem 'sidetiq', '0.6.3'
# HTTP requests
gem "httparty"
@@ -142,7 +140,7 @@ gem "redis-rails"
gem 'tinder', '~> 1.9.2'
# HipChat integration
-gem "hipchat", "~> 1.4.0"
+gem 'hipchat', '~> 1.5.0'
# Flowdock integration
gem "gitlab-flowdock-git-hook", "~> 0.4.2"
@@ -157,7 +155,7 @@ gem "slack-notifier", "~> 1.0.0"
gem 'asana', '~> 0.0.6'
# d3
-gem "d3_rails", "~> 3.1.4"
+gem 'd3_rails', '~> 3.5.5'
#cal-heatmap
gem "cal-heatmap-rails", "~> 0.0.1"
@@ -177,8 +175,8 @@ gem 'ace-rails-ap'
# Keyboard shortcuts
gem 'mousetrap-rails'
-# Shutting down requests that take too long
-gem "slowpoke"
+# Detect and convert string character encoding
+gem 'charlock_holmes'
gem "sass-rails", '~> 4.0.2'
gem "coffee-rails"
@@ -187,14 +185,14 @@ gem 'turbolinks'
gem 'jquery-turbolinks'
gem 'select2-rails'
-gem 'jquery-atwho-rails', "~> 0.3.3"
+gem 'jquery-atwho-rails', '~> 1.0.0'
gem "jquery-rails"
gem "jquery-ui-rails"
gem "jquery-scrollto-rails"
gem "raphael-rails", "~> 2.1.2"
gem 'bootstrap-sass', '~> 3.0'
gem "font-awesome-rails", '~> 4.2'
-gem "gitlab_emoji", "~> 0.0.1.1"
+gem "gitlab_emoji", "~> 0.1"
gem "gon", '~> 5.0.0'
gem 'nprogress-rails'
gem 'request_store'
@@ -207,7 +205,7 @@ group :development do
gem "letter_opener"
gem 'quiet_assets', '~> 1.0.1'
gem 'rack-mini-profiler', require: false
- gem "byebug"
+ gem 'rerun', '~> 0.10.0'
# Better errors handler
gem 'better_errors'
@@ -223,14 +221,13 @@ end
group :development, :test do
gem 'coveralls', require: false
gem 'rubocop', '0.28.0', require: false
- # gem 'rails-dev-tweaks'
gem 'spinach-rails'
gem "rspec-rails", '2.99'
- gem "capybara", '~> 2.2.1'
+ gem 'capybara', '~> 2.2.1'
+ gem 'capybara-screenshot', '~> 1.0.0'
gem "pry-rails"
gem "awesome_print"
gem "database_cleaner"
- gem "launchy"
gem 'factory_girl_rails'
# Prevent occasions where minitest is not bundled in packaged versions of ruby (see #3826)
@@ -251,11 +248,13 @@ group :development, :test do
# PhantomJS driver for Capybara
gem 'poltergeist', '~> 1.5.1'
- gem 'jasmine', '2.0.2'
+ gem 'jasmine-rails'
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
@@ -268,7 +267,6 @@ end
group :production do
gem "gitlab_meta", '7.0'
- gem "therubyracer"
end
gem "newrelic_rpm"
diff --git a/Gemfile.lock b/Gemfile.lock
index cca8f59ac28..6f58c4f4fda 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)
@@ -47,7 +47,7 @@ GEM
astrolabe (1.3.0)
parser (>= 2.2.0.pre.3, < 3.0)
attr_required (1.0.0)
- autoprefixer-rails (5.1.6)
+ autoprefixer-rails (5.1.11)
execjs
json
awesome_print (1.2.0)
@@ -60,7 +60,7 @@ GEM
erubis (>= 2.6.6)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
- bootstrap-sass (3.3.3)
+ bootstrap-sass (3.3.4.1)
autoprefixer-rails (>= 5.0.0.1)
sass (>= 3.2.19)
brakeman (3.0.1)
@@ -85,6 +85,9 @@ GEM
rack (>= 1.0.0)
rack-test (>= 0.5.4)
xpath (~> 2.0)
+ capybara-screenshot (1.0.9)
+ capybara (>= 1.0, < 3)
+ launchy
carrierwave (0.9.0)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
@@ -116,7 +119,7 @@ GEM
crack (0.4.1)
safe_yaml (~> 0.9.0)
creole (0.3.8)
- d3_rails (3.1.10)
+ d3_rails (3.5.5)
railties (>= 3.1.0)
daemons (1.1.9)
database_cleaner (1.3.0)
@@ -136,25 +139,22 @@ GEM
diff-lcs (1.2.5)
diffy (3.0.3)
docile (1.1.5)
- doorkeeper (2.1.0)
- railties (>= 3.1)
+ doorkeeper (2.1.3)
+ railties (>= 3.2)
dotenv (0.9.0)
dropzonejs-rails (0.4.14)
rails (> 3.1)
email_spec (1.5.0)
launchy (~> 2.1)
mail (~> 2.2)
- emoji (1.0.1)
- json
enumerize (0.7.0)
activesupport (>= 3.2)
equalizer (0.0.8)
- errbase (0.0.2)
erubis (2.7.0)
escape_utils (0.2.4)
eventmachine (1.0.4)
excon (0.32.1)
- execjs (2.0.2)
+ execjs (2.5.2)
expression_parser (0.9.0)
factory_girl (4.3.0)
activesupport (>= 3.0.0)
@@ -167,7 +167,7 @@ GEM
faraday (>= 0.7.4, < 0.9)
fastercsv (1.5.5)
ffaker (1.22.1)
- ffi (1.9.3)
+ ffi (1.9.8)
fog (1.21.0)
fog-brightbox
fog-core (~> 1.21, >= 1.21.1)
@@ -191,8 +191,10 @@ GEM
dotenv (>= 0.7)
thor (>= 0.13.6)
formatador (0.2.4)
- gemnasium-gitlab-service (0.2.5)
+ gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
+ gemojione (2.0.0)
+ json
gherkin-ruby (0.3.1)
racc
github-markup (1.3.1)
@@ -200,7 +202,7 @@ GEM
gitlab-flowdock-git-hook (0.4.2.2)
gitlab-grit (>= 2.4.1)
multi_json
- gitlab-grack (2.0.0.rc2)
+ gitlab-grack (2.0.2)
rack (~> 1.5.1)
gitlab-grit (2.7.2)
charlock_holmes (~> 0.6)
@@ -211,24 +213,24 @@ GEM
charlock_holmes (~> 0.6.6)
escape_utils (~> 0.2.4)
mime-types (~> 1.19)
- gitlab_emoji (0.0.1.1)
- emoji (~> 1.0.1)
- gitlab_git (7.0.1)
+ gitlab_emoji (0.1.0)
+ gemojione (~> 2.0)
+ gitlab_git (7.1.10)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-linguist (~> 3.0)
rugged (~> 0.21.2)
gitlab_meta (7.0)
- gitlab_omniauth-ldap (1.2.0)
+ gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
pyu-ruby-sasl (~> 0.0.3.1)
rubyntlm (~> 0.3)
- gollum-grit_adapter (0.1.0)
- gitlab-grit (~> 2.7.1)
- gollum-lib (4.0.0)
+ gollum-grit_adapter (0.1.3)
+ gitlab-grit (~> 2.7, >= 2.7.1)
+ gollum-lib (4.0.2)
github-markup (~> 1.3.1)
- gollum-grit_adapter (~> 0.1.0)
+ gollum-grit_adapter (~> 0.1, >= 0.1.1)
nokogiri (~> 1.6.4)
rouge (~> 1.7.4)
sanitize (~> 2.1.0)
@@ -272,32 +274,29 @@ GEM
hashie (2.1.2)
highline (1.6.21)
hike (1.2.3)
- hipchat (1.4.0)
+ hipchat (1.5.0)
httparty
+ mimemagic
hitimes (1.2.2)
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.4)
- html-pipeline-gitlab (0.1.5)
- actionpack (~> 4)
- gitlab_emoji (~> 0.0.1)
- html-pipeline (~> 1.11.0)
- sanitize (~> 2.1)
http_parser.rb (0.5.3)
- httparty (0.13.0)
+ httparty (0.13.3)
json (~> 1.8)
multi_xml (>= 0.5.2)
httpauth (0.2.1)
httpclient (2.5.3.3)
i18n (0.7.0)
+ ice_cube (0.11.1)
ice_nine (0.10.0)
- jasmine (2.0.2)
- jasmine-core (~> 2.0.0)
- phantomjs
- rack (>= 1.2.1)
- rake
- jasmine-core (2.0.0)
- jquery-atwho-rails (0.3.3)
+ jasmine-core (2.2.0)
+ jasmine-rails (0.10.8)
+ jasmine-core (>= 1.3, < 3.0)
+ phantomjs (>= 1.9)
+ railties (>= 3.2.0)
+ sprockets-rails
+ jquery-atwho-rails (1.0.1)
jquery-rails (3.1.0)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
@@ -319,9 +318,8 @@ GEM
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
- libv8 (3.16.14.7)
- listen (2.3.1)
- celluloid (>= 0.15.2)
+ listen (2.10.0)
+ celluloid (~> 0.16.0)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
lumberjack (1.0.4)
@@ -329,6 +327,7 @@ GEM
mime-types (>= 1.16, < 3)
method_source (0.8.2)
mime-types (1.25.1)
+ mimemagic (0.3.0)
mini_portile (0.6.1)
minitest (5.3.5)
mousetrap-rails (1.4.6)
@@ -336,7 +335,7 @@ GEM
multi_xml (0.5.5)
multipart-post (1.2.0)
mysql2 (0.3.16)
- net-ldap (0.9.0)
+ net-ldap (0.11)
net-scp (1.1.2)
net-ssh (>= 2.6.5)
net-ssh (2.8.0)
@@ -391,7 +390,7 @@ GEM
parser (2.2.0.2)
ast (>= 1.1, < 3.0)
pg (0.15.1)
- phantomjs (1.9.2.0)
+ phantomjs (1.9.8.0)
poltergeist (1.5.1)
capybara (~> 2.1)
cliver (~> 0.3.1)
@@ -429,7 +428,6 @@ GEM
rack
rack-test (0.6.3)
rack (>= 1.0)
- rack-timeout (0.2.0)
rails (4.1.9)
actionmailer (= 4.1.9)
actionpack (= 4.1.9)
@@ -453,12 +451,12 @@ GEM
raindrops (0.13.0)
rake (10.4.2)
raphael-rails (2.1.2)
- rb-fsevent (0.9.3)
- rb-inotify (0.9.2)
+ rb-fsevent (0.9.4)
+ rb-inotify (0.9.5)
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)
@@ -478,14 +476,13 @@ GEM
redis-store (~> 1.1.0)
redis-store (1.1.4)
redis (>= 2.2)
- ref (1.0.5)
request_store (1.0.5)
+ rerun (0.10.0)
+ listen (~> 2.7, >= 2.7.3)
rest-client (1.6.7)
mime-types (>= 1.16)
rinku (1.7.3)
- robustly (0.0.3)
- errbase
- rouge (1.7.4)
+ rouge (1.7.7)
rspec (2.99.0)
rspec-core (~> 2.99.0)
rspec-expectations (~> 2.99.0)
@@ -517,10 +514,10 @@ GEM
sexp_processor (~> 4.0)
ruby_parser (3.5.0)
sexp_processor (~> 4.1)
- rubyntlm (0.4.0)
+ rubyntlm (0.5.0)
rubypants (0.2.0)
rugged (0.21.4)
- rugments (1.0.0.beta4)
+ rugments (1.0.0.beta6)
safe_yaml (0.9.7)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
@@ -551,6 +548,10 @@ GEM
json
redis (>= 3.0.6)
redis-namespace (>= 1.3.1)
+ sidetiq (0.6.3)
+ celluloid (>= 0.14.1)
+ ice_cube (= 0.11.1)
+ sidekiq (>= 3.0.0)
simple_oauth (0.1.9)
simplecov (0.9.0)
docile (~> 1.1.0)
@@ -567,9 +568,6 @@ GEM
temple (~> 0.6.6)
tilt (>= 1.3.3, < 2.1)
slop (3.6.0)
- slowpoke (0.0.5)
- rack-timeout (>= 0.1.0)
- robustly
spinach (0.8.7)
colorize (= 0.5.8)
gherkin-ruby (>= 0.3.1)
@@ -594,20 +592,19 @@ GEM
stamp (0.5.0)
state_machine (1.2.0)
stringex (2.5.2)
+ task_list (1.0.2)
+ html-pipeline
temple (0.6.7)
term-ansicolor (1.2.2)
tins (~> 0.8)
terminal-table (1.4.5)
test_after_commit (0.2.2)
- therubyracer (0.12.0)
- libv8 (~> 3.16.14.0)
- ref
thin (1.6.1)
daemons (>= 1.0.9)
eventmachine (>= 1.0.0)
rack (>= 1.0.0)
thor (0.19.1)
- thread_safe (0.3.4)
+ thread_safe (0.3.5)
tilt (1.4.1)
timers (4.0.1)
hitimes
@@ -668,7 +665,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)
@@ -682,18 +679,20 @@ DEPENDENCIES
byebug
cal-heatmap-rails (~> 0.0.1)
capybara (~> 2.2.1)
+ capybara-screenshot (~> 1.0.0)
carrierwave
+ charlock_holmes
coffee-rails
colored
coveralls
creole (~> 0.3.6)
- d3_rails (~> 3.1.4)
+ d3_rails (~> 3.5.5)
database_cleaner
default_value_for (~> 3.0.0)
devise (= 3.2.4)
devise-async (= 0.9.0)
diffy (~> 3.0.3)
- doorkeeper (= 2.1.0)
+ doorkeeper (= 2.1.3)
dropzonejs-rails
email_spec
enumerize
@@ -705,13 +704,13 @@ DEPENDENCIES
gemnasium-gitlab-service (~> 0.2)
github-markup
gitlab-flowdock-git-hook (~> 0.4.2)
- gitlab-grack (~> 2.0.0.rc2)
+ gitlab-grack (~> 2.0.2)
gitlab-linguist (~> 3.0.1)
- gitlab_emoji (~> 0.0.1.1)
- gitlab_git (= 7.0.1)
+ gitlab_emoji (~> 0.1)
+ gitlab_git (~> 7.1.10)
gitlab_meta (= 7.0)
- gitlab_omniauth-ldap (= 1.2.0)
- gollum-lib (~> 4.0.0)
+ gitlab_omniauth-ldap (= 1.2.1)
+ gollum-lib (~> 4.0.2)
gon (~> 5.0.0)
grape (~> 0.6.1)
grape-entity (~> 0.4.2)
@@ -719,17 +718,16 @@ DEPENDENCIES
guard-rspec
guard-spinach
haml-rails
- hipchat (~> 1.4.0)
- html-pipeline-gitlab (~> 0.1.0)
+ hipchat (~> 1.5.0)
+ html-pipeline (~> 1.11.0)
httparty
- jasmine (= 2.0.2)
- jquery-atwho-rails (~> 0.3.3)
+ jasmine-rails
+ jquery-atwho-rails (~> 1.0.0)
jquery-rails
jquery-scrollto-rails
jquery-turbolinks
jquery-ui-rails
kaminari (~> 0.15.1)
- launchy
letter_opener
minitest (~> 5.3.0)
mousetrap-rails
@@ -760,9 +758,10 @@ DEPENDENCIES
rb-fsevent
rb-inotify
rdoc (~> 3.6)
- redcarpet (~> 3.1.2)
+ redcarpet (~> 3.2.3)
redis-rails
request_store
+ rerun (~> 0.10.0)
rspec-rails (= 2.99)
rubocop (= 0.28.0)
rugments
@@ -774,20 +773,20 @@ DEPENDENCIES
settingslogic
shoulda-matchers (~> 2.7.0)
sidekiq (~> 3.3)
+ sidetiq (= 0.6.3)
simplecov
sinatra
six
slack-notifier (~> 1.0.0)
slim
- slowpoke
spinach-rails
spring (~> 1.3.1)
spring-commands-rspec (= 1.0.4)
spring-commands-spinach (= 1.0.0)
stamp
state_machine
+ task_list (~> 1.0.0)
test_after_commit
- therubyracer
thin
tinder (~> 1.9.2)
turbolinks
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/MAINTENANCE.md b/MAINTENANCE.md
index 19200fef389..d3d36670693 100644
--- a/MAINTENANCE.md
+++ b/MAINTENANCE.md
@@ -2,7 +2,7 @@
GitLab is a fast moving and evolving project. We currently don't have the resources to support many releases concurrently. We support exactly one stable release at any given time.
-GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: `(Major).(Minor).(Patch)`.
+GitLab follows the [Semantic Versioning](http://semver.org/) for its releases: `(Major).(Minor).(Patch)` in a [pragmatic way](https://gist.github.com/jashkenas/cbd2b088e20279ae2c8e).
- **Major version**: Whenever there is something significant or any backwards incompatible changes are introduced to the public API.
- **Minor version**: When new, backwards compatible functionality is introduced to the public API or a minor feature is introduced, or when a set of smaller features is rolled out.
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/README.md b/README.md
index 0563ceca409..130351b15b8 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,7 @@
+## Canonical source
+
+The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible.
+
# ![logo](https://about.gitlab.com/images/gitlab_logo.png) GitLab
## Open source software to collaborate on code
@@ -19,10 +23,6 @@ There are two editions of GitLab.
*GitLab Enterprise Edition (EE)* includes [extra features](https://about.gitlab.com/features/#compare) that are most useful for organizations with more than 100 users.
To get access to the EE and support please [become a subscriber](https://about.gitlab.com/pricing/).
-## Canonical source
-
-The source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/) and there are mirrors to make [contributing](CONTRIBUTING.md) as easy as possible.
-
## Code status
- [![build status](https://ci.gitlab.org/projects/1/status.png?ref=master)](https://ci.gitlab.org/projects/1?ref=master) on ci.gitlab.org (master branch)
diff --git a/VERSION b/VERSION
index e5d25bf79a9..e85691e6ff7 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-7.9.0.pre
+7.11.0.pre
diff --git a/app/assets/images/authbuttons/bitbucket_32.png b/app/assets/images/authbuttons/bitbucket_32.png
deleted file mode 100644
index 27702eb973d..00000000000
--- a/app/assets/images/authbuttons/bitbucket_32.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/authbuttons/github_32.png b/app/assets/images/authbuttons/github_32.png
deleted file mode 100644
index 0445b567bbc..00000000000
--- a/app/assets/images/authbuttons/github_32.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/authbuttons/gitlab_32.png b/app/assets/images/authbuttons/gitlab_32.png
deleted file mode 100644
index f3b78cb6efb..00000000000
--- a/app/assets/images/authbuttons/gitlab_32.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/authbuttons/gitlab_64.png b/app/assets/images/authbuttons/gitlab_64.png
index ff2945fe89e..31281a19444 100644
--- a/app/assets/images/authbuttons/gitlab_64.png
+++ b/app/assets/images/authbuttons/gitlab_64.png
Binary files differ
diff --git a/app/assets/images/authbuttons/google_32.png b/app/assets/images/authbuttons/google_32.png
deleted file mode 100644
index b03c3ec5207..00000000000
--- a/app/assets/images/authbuttons/google_32.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/authbuttons/twitter_32.png b/app/assets/images/authbuttons/twitter_32.png
deleted file mode 100644
index a3d4964f40f..00000000000
--- a/app/assets/images/authbuttons/twitter_32.png
+++ /dev/null
Binary files differ
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 c7acde2afe5..bb9da147018 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -38,7 +38,7 @@
#= require shortcuts
#= require shortcuts_navigation
#= require shortcuts_dashboard_navigation
-#= require shortcuts_issueable
+#= require shortcuts_issuable
#= require shortcuts_network
#= require cal-heatmap
#= require_tree .
@@ -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()
@@ -132,10 +132,17 @@ $ ->
), 1
# Initialize tooltips
- $('.has_tooltip').tooltip()
-
- # Bottom tooltip
- $('.has_bottom_tooltip').tooltip(placement: 'bottom')
+ $('body').tooltip({
+ selector: '.has_tooltip, [data-toggle="tooltip"], .page-sidebar-collapsed .nav-sidebar a'
+ placement: (_, el) ->
+ $el = $(el)
+ if $el.attr('id') == 'js-shortcuts-home'
+ # Place the logo tooltip on the right when collapsed, bottom when expanded
+ $el.parents('header').hasClass('header-collapsed') and 'right' or 'bottom'
+ else
+ # Otherwise use the data-placement attribute like normal
+ $el.data('placement')
+ })
# Form submitter
$('.trigger-submit').on 'change', ->
@@ -169,12 +176,11 @@ $ ->
# Show/hide comments on diff
$("body").on "click", ".js-toggle-diff-comments", (e) ->
- $(@).find('i').
- toggleClass('fa fa-chevron-down').
- toggleClass('fa fa-chevron-up')
+ $(@).toggleClass('active')
$(@).closest(".diff-file").find(".notes_holder").toggle()
e.preventDefault()
+ $(document).off "click", '.js-confirm-danger'
$(document).on "click", '.js-confirm-danger', (e) ->
e.preventDefault()
btn = $(e.target)
diff --git a/app/assets/javascripts/behaviors/taskable.js.coffee b/app/assets/javascripts/behaviors/taskable.js.coffee
deleted file mode 100644
index ddce71c1886..00000000000
--- a/app/assets/javascripts/behaviors/taskable.js.coffee
+++ /dev/null
@@ -1,21 +0,0 @@
-window.updateTaskState = (taskableType) ->
- objType = taskableType.data
- isChecked = $(this).prop("checked")
- if $(this).is(":checked")
- stateEvent = "task_check"
- else
- stateEvent = "task_uncheck"
-
- taskableUrl = $("form.edit-" + objType).first().attr("action")
- taskableNum = taskableUrl.match(/\d+$/)
- taskNum = 0
- $("li.task-list-item input:checkbox").each( (index, e) =>
- if e == this
- taskNum = index + 1
- )
-
- $.ajax
- type: "PATCH"
- url: taskableUrl
- data: objType + "[state_event]=" + stateEvent +
- "&" + objType + "[task_num]=" + taskNum
diff --git a/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee b/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee
deleted file mode 100644
index 691ed4f98ae..00000000000
--- a/app/assets/javascripts/behaviors/toggle_diff_line_wrap_behavior.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-$ ->
- # Toggle line wrapping in diff.
- #
- # %div.diff-file
- # %input.js-toggle-diff-line-wrap
- # %td.line_content
- #
- $("body").on "click", ".js-toggle-diff-line-wrap", (e) ->
- diffFile = $(@).closest(".diff-file")
- if $(@).is(":checked")
- diffFile.addClass("diff-wrap-lines")
- else
- diffFile.removeClass("diff-wrap-lines")
-
diff --git a/app/assets/javascripts/blob/blob.js.coffee b/app/assets/javascripts/blob/blob.js.coffee
index a5f15f80c5c..37a175fdbc7 100644
--- a/app/assets/javascripts/blob/blob.js.coffee
+++ b/app/assets/javascripts/blob/blob.js.coffee
@@ -26,7 +26,7 @@ class @BlobView
unless isNaN first_line
$("#tree-content-holder .highlight .line").removeClass("hll")
$("#LC#{line}").addClass("hll") for line in [first_line..last_line]
- $.scrollTo("#L#{first_line}") unless e?
+ $.scrollTo("#L#{first_line}", offset: -50) unless e?
# parse selected lines from hash
# always return first and last line (initialized to NaN)
diff --git a/app/assets/javascripts/branch-graph.js.coffee b/app/assets/javascripts/branch-graph.js.coffee
index 010a2b0e42b..917228bd276 100644
--- a/app/assets/javascripts/branch-graph.js.coffee
+++ b/app/assets/javascripts/branch-graph.js.coffee
@@ -214,7 +214,7 @@ class @BranchGraph
stroke: @colors[commit.space]
"stroke-width": 2
)
- r.image(gon.relative_url_root + commit.author.icon, avatar_box_x, avatar_box_y, 20, 20)
+ r.image(commit.author.icon, avatar_box_x, avatar_box_y, 20, 20)
r.text(@offsetX + @unitSpace * @mspace + 35, y, commit.message.split("\n")[0]).attr(
"text-anchor": "start"
font: "14px Monaco, monospace"
diff --git a/app/assets/javascripts/calendar.js.coffee b/app/assets/javascripts/calendar.js.coffee
index 19ea4ccc4cf..44d75bd694f 100644
--- a/app/assets/javascripts/calendar.js.coffee
+++ b/app/assets/javascripts/calendar.js.coffee
@@ -1,13 +1,13 @@
-class @calendar
+class @Calendar
options =
month: "short"
day: "numeric"
year: "numeric"
- constructor: (timestamps, starting_year, starting_month) ->
+ constructor: (timestamps, starting_year, starting_month, calendar_activities_path) ->
cal = new CalHeatMap()
cal.init
- itemName: ["commit"]
+ itemName: ["contribution"]
data: timestamps
start: new Date(starting_year, starting_month)
domainLabelFormat: "%b"
@@ -20,11 +20,19 @@ class @calendar
position: "top"
legend: [
0
- 1
- 4
- 7
+ 10
+ 20
+ 30
]
legendCellPadding: 3
onClick: (date, count) ->
- return
- return
+ formated_date = date.getFullYear() + "-" + (date.getMonth()+1) + "-" + date.getDate()
+ $.ajax
+ url: calendar_activities_path
+ data:
+ date: formated_date
+ cache: false
+ dataType: "html"
+ success: (data) ->
+ $(".user-calendar-activities").html data
+
diff --git a/app/assets/javascripts/confirm_danger_modal.js.coffee b/app/assets/javascripts/confirm_danger_modal.js.coffee
index bb99edbd09e..66e34dd4a08 100644
--- a/app/assets/javascripts/confirm_danger_modal.js.coffee
+++ b/app/assets/javascripts/confirm_danger_modal.js.coffee
@@ -8,11 +8,13 @@ class @ConfirmDangerModal
submit = $('.js-confirm-danger-submit')
submit.disable()
+ $('.js-confirm-danger-input').off 'input'
$('.js-confirm-danger-input').on 'input', ->
if rstrip($(@).val()) is project_path
submit.enable()
else
submit.disable()
+ $('.js-confirm-danger-submit').off 'click'
$('.js-confirm-danger-submit').on 'click', =>
@form.submit()
diff --git a/app/assets/javascripts/dashboard.js.coffee b/app/assets/javascripts/dashboard.js.coffee
index 3bdb9469d06..00ee503ff16 100644
--- a/app/assets/javascripts/dashboard.js.coffee
+++ b/app/assets/javascripts/dashboard.js.coffee
@@ -1,15 +1,3 @@
class @Dashboard
constructor: ->
- @initSidebarTab()
new ProjectsList()
-
- initSidebarTab: ->
- key = "dashboard_sidebar_filter"
-
- # store selection in cookie
- $('.dash-sidebar-tabs a').on 'click', (e) ->
- $.cookie(key, $(e.target).attr('id'))
-
- # show tab from cookie
- sidebar_filter = $.cookie(key)
- $("#" + sidebar_filter).tab('show') if sidebar_filter
diff --git a/app/assets/javascripts/diff.js.coffee b/app/assets/javascripts/diff.js.coffee
index 05f5af42571..069f91c30e1 100644
--- a/app/assets/javascripts/diff.js.coffee
+++ b/app/assets/javascripts/diff.js.coffee
@@ -37,8 +37,6 @@ class @Diff
)
)
- $('.diff-header').stick_in_parent(recalc_every: 1, offset_top: $('.navbar').height())
-
lineNumbers: (line) ->
return ([0, 0]) unless line.children().length
lines = line.children().slice(0, 2)
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 928232e95bd..06787ddf874 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -8,7 +8,6 @@ class Dispatcher
initPageScripts: ->
page = $('body').attr('data-page')
- project_id = $('body').attr('data-project-id')
unless page
return false
@@ -22,12 +21,14 @@ class Dispatcher
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
- shortcut_handler = new ShortcutsIssueable()
+ shortcut_handler = new ShortcutsIssuable()
new ZenMode()
when 'projects:milestones:show'
new Milestone()
when 'projects:milestones:new', 'projects:milestones:edit'
new ZenMode()
+ when 'projects:compare:show'
+ new Diff()
when 'projects:issues:new','projects:issues:edit'
GitLab.GfmAutoComplete.setup()
shortcut_handler = new ShortcutsNavigation()
@@ -45,13 +46,14 @@ class Dispatcher
new IssuableForm($('.merge-request-form'))
when 'projects:merge_requests:show'
new Diff()
- shortcut_handler = new ShortcutsIssueable()
+ shortcut_handler = new ShortcutsIssuable()
new ZenMode()
when "projects:merge_requests:diffs"
new Diff()
new ZenMode()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
+ MergeRequests.init()
when 'dashboard:show'
new Dashboard()
new Activities()
@@ -72,9 +74,12 @@ class Dispatcher
new Activities()
shortcut_handler = new ShortcutsNavigation()
new ProjectsList()
- when 'groups:members'
+ when 'groups:group_members:index'
new GroupMembers()
new UsersSelect()
+ when 'projects:project_members:index'
+ new ProjectMembers()
+ new UsersSelect()
when 'groups:new', 'groups:edit', 'admin:groups:edit'
new GroupAvatar()
when 'projects:tree:show'
@@ -93,6 +98,9 @@ class Dispatcher
new ProjectFork()
when 'users:show'
new User()
+ new Activities()
+ when 'admin:users:show'
+ new ProjectsList()
switch path.first()
when 'admin'
@@ -110,6 +118,8 @@ class Dispatcher
new Project()
new ProjectAvatar()
switch path[1]
+ when 'compare'
+ shortcut_handler = new ShortcutsNavigation()
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
@@ -118,7 +128,7 @@ class Dispatcher
when 'show'
new ProjectShow()
when 'issues', 'merge_requests'
- new ProjectUsersSelect()
+ new UsersSelect()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
@@ -126,9 +136,8 @@ class Dispatcher
new DropzoneInput($('.wiki-form'))
when 'snippets', 'labels', 'graphs'
shortcut_handler = new ShortcutsNavigation()
- when 'team_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
+ when 'project_members', 'deploy_keys', 'hooks', 'services', 'protected_branches'
shortcut_handler = new ShortcutsNavigation()
- new UsersSelect()
# If we haven't installed a custom shortcut handler, install the default one
diff --git a/app/assets/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index 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/gfm_auto_complete.js.coffee b/app/assets/javascripts/gfm_auto_complete.js.coffee
index 00d56ae5b4b..4eb3f3c03f3 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.coffee
+++ b/app/assets/javascripts/gfm_auto_complete.js.coffee
@@ -2,19 +2,19 @@
window.GitLab ?= {}
GitLab.GfmAutoComplete =
- # private_token: ''
dataSource: ''
+
# Emoji
Emoji:
- template: '<li data-value="${insert}">${name} <img alt="${name}" height="20" src="${image}" width="20" /></li>'
+ template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
# Team Members
Members:
- template: '<li data-value="${username}">${username} <small>${name}</small></li>'
+ template: '<li>${username} <small>${name}</small></li>'
# Issues and MergeRequests
Issues:
- template: '<li data-value="${id}"><small>${id}</small> ${title} </li>'
+ template: '<li><small>${id}</small> ${title}</li>'
# Add GFM auto-completion to all input fields, that accept GFM input.
setup: ->
@@ -23,45 +23,46 @@ GitLab.GfmAutoComplete =
# Emoji
input.atwho
at: ':'
- tpl: @Emoji.template
- callbacks:
- before_save: (emojis) =>
- $.map emojis, (em) => name: em.name, insert: em.name+ ':', image: em.path
+ displayTpl: @Emoji.template
+ insertTpl: ':${name}:'
# Team Members
input.atwho
at: '@'
- tpl: @Members.template
- search_key: 'search'
+ displayTpl: @Members.template
+ insertTpl: '${atwho-at}${username}'
+ searchKey: 'search'
callbacks:
- before_save: (members) =>
- $.map members, (m) => name: m.name, username: m.username, search: "#{m.username} #{m.name}"
+ beforeSave: (members) ->
+ $.map members, (m) -> name: m.name, username: m.username, search: "#{m.username} #{m.name}"
input.atwho
at: '#'
alias: 'issues'
- search_key: 'search'
- tpl: @Issues.template
+ searchKey: 'search'
+ displayTpl: @Issues.template
+ insertTpl: '${atwho-at}${id}'
callbacks:
- before_save: (issues) ->
+ beforeSave: (issues) ->
$.map issues, (i) -> id: i.iid, title: sanitize(i.title), search: "#{i.iid} #{i.title}"
input.atwho
at: '!'
alias: 'mergerequests'
- search_key: 'search'
- tpl: @Issues.template
+ searchKey: 'search'
+ displayTpl: @Issues.template
+ insertTpl: '${atwho-at}${id}'
callbacks:
- before_save: (merges) ->
+ beforeSave: (merges) ->
$.map merges, (m) -> id: m.iid, title: sanitize(m.title), search: "#{m.iid} #{m.title}"
- input.one "focus", =>
+ input.one 'focus', =>
$.getJSON(@dataSource).done (data) ->
# load members
- input.atwho 'load', "@", data.members
+ input.atwho 'load', '@', data.members
# load issues
- input.atwho 'load', "issues", data.issues
+ input.atwho 'load', 'issues', data.issues
# load merge requests
- input.atwho 'load', "mergerequests", data.mergerequests
+ input.atwho 'load', 'mergerequests', data.mergerequests
# load emojis
- input.atwho 'load', ":", data.emojis
+ input.atwho 'load', ':', data.emojis
diff --git a/app/assets/javascripts/importer_status.js.coffee b/app/assets/javascripts/importer_status.js.coffee
index e0e7771ab20..be8d225e73b 100644
--- a/app/assets/javascripts/importer_status.js.coffee
+++ b/app/assets/javascripts/importer_status.js.coffee
@@ -16,20 +16,20 @@ class @ImporterStatus
$(".js-import-all").click (event) =>
$(".js-add-to-import").each ->
$(this).click()
-
+
setAutoUpdate: ->
setInterval (=>
$.get @jobs_url, (data) =>
$.each data, (i, job) =>
job_item = $("#project_" + job.id)
status_field = job_item.find(".job-status")
-
+
if job.import_status == 'finished'
job_item.removeClass("active").addClass("success")
- status_field.html('<span class="cgreen"><i class="fa fa-check"></i> done</span>')
+ status_field.html('<span><i class="fa fa-check"></i> done</span>')
else if job.import_status == 'started'
status_field.html("<i class='fa fa-spinner fa-spin'></i> started")
else
status_field.html(job.import_status)
-
- ), 4000 \ No newline at end of file
+
+ ), 4000
diff --git a/app/assets/javascripts/issue.js.coffee b/app/assets/javascripts/issue.js.coffee
index f2753170478..86ad3d03bac 100644
--- a/app/assets/javascripts/issue.js.coffee
+++ b/app/assets/javascripts/issue.js.coffee
@@ -1,3 +1,7 @@
+#= require jquery
+#= require jquery.waitforimages
+#= require task_list
+
class @Issue
constructor: ->
$('.edit-issue.inline-update input[type="submit"]').hide()
@@ -6,19 +10,38 @@ class @Issue
$(".context .inline-update").on "change", "#issue_assignee_id", ->
$(this).submit()
- if $("a.btn-close").length
- $("li.task-list-item input:checkbox").prop("disabled", false)
+ # Prevent duplicate event bindings
+ @disableTaskList()
- $(".task-list-item input:checkbox").on(
- "click"
- null
- "issue"
- updateTaskState
- )
+ if $("a.btn-close").length
+ @initTaskList()
$('.issue-details').waitForImages ->
$('.issuable-affix').affix offset:
top: ->
- @top = $('.issue-details').outerHeight(true) + 25
+ @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('')
+
+ initTaskList: ->
+ $('.issue-details .js-task-list-container').taskList('enable')
+ $(document).on 'tasklist:changed', '.issue-details .js-task-list-container', @updateTaskList
+
+ disableTaskList: ->
+ $('.issue-details .js-task-list-container').taskList('disable')
+ $(document).off 'tasklist:changed', '.issue-details .js-task-list-container'
+
+ # TODO (rspeicher): Make the issue description inline-editable like a note so
+ # that we can re-use its form here
+ updateTaskList: ->
+ patchData = {}
+ patchData['issue'] = {'description': $('.js-task-list-field', this).val()}
+
+ $.ajax
+ type: 'PATCH'
+ url: $('form.js-issue-update').attr('action')
+ data: patchData
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index 6513f4bcefc..40bb9e9cb0c 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -47,7 +47,7 @@
initSearch: ->
@timer = null
$("#issue_search").keyup ->
- clearTimeout(@timer);
+ clearTimeout(@timer)
@timer = setTimeout(Issues.filterResults, 500)
filterResults: =>
diff --git a/app/assets/javascripts/merge_request.js.coffee b/app/assets/javascripts/merge_request.js.coffee
index 1fee9dc1892..7c1e2b822d7 100644
--- a/app/assets/javascripts/merge_request.js.coffee
+++ b/app/assets/javascripts/merge_request.js.coffee
@@ -1,3 +1,7 @@
+#= require jquery
+#= require bootstrap
+#= require task_list
+
class @MergeRequest
constructor: (@opts) ->
@initContextWidget()
@@ -17,15 +21,22 @@ class @MergeRequest
disableButtonIfEmptyField '#commit_message', '.accept_merge_request'
+ # Prevent duplicate event bindings
+ @disableTaskList()
+
if $("a.btn-close").length
- $("li.task-list-item input:checkbox").prop("disabled", false)
+ @initTaskList()
$('.merge-request-details').waitForImages ->
$('.issuable-affix').affix offset:
top: ->
- @top = $('.merge-request-details').outerHeight(true) + 91
+ @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 +65,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,13 +84,6 @@ 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
- )
-
activateTab: (action) ->
this.$('.merge-request-tabs li').removeClass 'active'
this.$('.tab-content').hide()
@@ -110,11 +106,17 @@ class @MergeRequest
showCiState: (state) ->
$('.ci_widget').hide()
- allowed_states = ["failed", "running", "pending", "success"]
+ allowed_states = ["failed", "canceled", "running", "pending", "success"]
if state in allowed_states
$('.ci_widget.ci-' + state).show()
+ switch state
+ when "failed", "canceled"
+ @setMergeButtonClass('btn-danger')
+ when "running", "pending"
+ @setMergeButtonClass('btn-warning')
else
$('.ci_widget.ci-error').show()
+ @setMergeButtonClass('btn-danger')
showCiCoverage: (coverage) ->
cov_html = $('<span>')
@@ -144,6 +146,9 @@ class @MergeRequest
this.$('.merge-in-progress').hide()
this.$('.automerge_widget.already_cannot_be_merged').show()
+ setMergeButtonClass: (css_class) ->
+ $('.accept_merge_request').removeClass("btn-create").addClass(css_class)
+
mergeInProgress: ->
$.ajax
type: 'GET'
@@ -156,3 +161,21 @@ class @MergeRequest
setTimeout(merge_request.mergeInProgress, 3000)
dataType: 'json'
+ initTaskList: ->
+ $('.merge-request-details .js-task-list-container').taskList('enable')
+ $(document).on 'tasklist:changed', '.merge-request-details .js-task-list-container', @updateTaskList
+
+ disableTaskList: ->
+ $('.merge-request-details .js-task-list-container').taskList('disable')
+ $(document).off 'tasklist:changed', '.merge-request-details .js-task-list-container'
+
+ # TODO (rspeicher): Make the merge request description inline-editable like a
+ # note so that we can re-use its form here
+ updateTaskList: ->
+ patchData = {}
+ patchData['merge_request'] = {'description': $('.js-task-list-field', this).val()}
+
+ $.ajax
+ type: 'PATCH'
+ url: $('form.js-merge-request-update').attr('action')
+ data: patchData
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
index 9201c84c5ed..83434c1b9ba 100644
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ b/app/assets/javascripts/merge_requests.js.coffee
@@ -1,8 +1,35 @@
#
# * Filter merge requests
#
-@merge_requestsPage = ->
- $('#assignee_id').select2()
- $('#milestone_id').select2()
- $('#milestone_id, #assignee_id').on 'change', ->
- $(this).closest('form').submit()
+@MergeRequests =
+ init: ->
+ MergeRequests.initSearch()
+
+ # Make sure we trigger ajax request only after user stop typing
+ initSearch: ->
+ @timer = null
+ $("#issue_search").keyup ->
+ clearTimeout(@timer)
+ @timer = setTimeout(MergeRequests.filterResults, 500)
+
+ filterResults: =>
+ form = $("#issue_search_form")
+ search = $("#issue_search").val()
+ $('.merge-requests-holder').css("opacity", '0.5')
+ issues_url = form.attr('action') + '? '+ form.serialize()
+
+ $.ajax
+ type: "GET"
+ url: form.attr('action')
+ data: form.serialize()
+ complete: ->
+ $('.merge-requests-holder').css("opacity", '1.0')
+ success: (data) ->
+ $('.merge-requests-holder').html(data.html)
+ # Change url so if user reload a page - search results are saved
+ History.replaceState {page: issues_url}, document.title, issues_url
+ MergeRequests.reload()
+ dataType: "json"
+
+ reload: ->
+ $('#filter_issue_search').val($('#issue_search').val())
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 90e6fd6d154..c25b1ddb066 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -1,3 +1,12 @@
+#= require jquery
+#= require autosave
+#= require bootstrap
+#= require dropzone
+#= require dropzone_input
+#= require gfm_auto_complete
+#= require jquery.atwho
+#= require task_list
+
class @Notes
@interval: null
@@ -11,6 +20,7 @@ class @Notes
@setupMainTargetNoteForm()
@cleanBinding()
@addBinding()
+ @initTaskList()
addBinding: ->
# add note to UI after creation
@@ -37,7 +47,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 +68,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 +82,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"
@@ -78,6 +91,9 @@ class @Notes
$(document).off "click", ".js-note-target-reopen"
$(document).off "click", ".js-note-target-close"
+ $('.note .js-task-list-container').taskList('disable')
+ $(document).off 'tasklist:changed', '.note .js-task-list-container'
+
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
@@ -111,6 +127,7 @@ class @Notes
if @isNewNote(note)
@note_ids.push(note.id)
$('ul.main-notes-list').append(note.html)
+ @initTaskList()
###
Check if note does not exists on page
@@ -169,6 +186,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.
@@ -260,6 +282,8 @@ class @Notes
note_li.replaceWith(note.html)
note_li.find('.note-edit-form').hide()
note_li.find('.note-body > .note-text').show()
+ note_li.find('js-task-list-container').taskList('enable')
+ @enableTaskList()
###
Called in response to clicking the edit note link
@@ -425,7 +449,7 @@ class @Notes
@removeDiscussionNoteForm(form)
updateVotes: ->
- (new NotesVotes).updateVotes()
+ true
###
Called after an attachment file has been selected.
@@ -471,3 +495,13 @@ class @Notes
else
form.find('.js-note-target-reopen').text('Reopen')
form.find('.js-note-target-close').text('Close')
+
+ initTaskList: ->
+ @enableTaskList()
+ $(document).on 'tasklist:changed', '.note .js-task-list-container', @updateTaskList
+
+ enableTaskList: ->
+ $('.note .js-task-list-container').taskList('enable')
+
+ updateTaskList: ->
+ $('form', this).submit()
diff --git a/app/assets/javascripts/notes_votes.js.coffee b/app/assets/javascripts/notes_votes.js.coffee
deleted file mode 100644
index 65c149b7886..00000000000
--- a/app/assets/javascripts/notes_votes.js.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-class @NotesVotes
- updateVotes: ->
- votes = $("#votes .votes")
- notes = $("#notes-list .note .vote")
-
- # only update if there is a vote display
- if votes.size()
- upvotes = notes.filter(".upvote").size()
- downvotes = notes.filter(".downvote").size()
- votesCount = upvotes + downvotes
- upvotesPercent = (if votesCount then (100.0 / votesCount * upvotes) else 0)
- downvotesPercent = (if votesCount then (100.0 - upvotesPercent) else 0)
-
- # change vote bar lengths
- votes.find(".bar-success").css "width", upvotesPercent + "%"
- votes.find(".bar-danger").css "width", downvotesPercent + "%"
-
- # replace vote numbers
- votes.find(".upvotes").text votes.find(".upvotes").text().replace(/\d+/, upvotes)
- votes.find(".downvotes").text votes.find(".downvotes").text().replace(/\d+/, downvotes)
diff --git a/app/assets/javascripts/project_members.js.coffee b/app/assets/javascripts/project_members.js.coffee
new file mode 100644
index 00000000000..896ba7e53ee
--- /dev/null
+++ b/app/assets/javascripts/project_members.js.coffee
@@ -0,0 +1,4 @@
+class @ProjectMembers
+ constructor: ->
+ $('li.project_member').bind 'ajax:success', ->
+ $(this).fadeOut()
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 e22c7c11f1c..00000000000
--- a/app/assets/javascripts/project_users_select.js.coffee
+++ /dev/null
@@ -1,59 +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 isnt ""
- 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
-
- if user.id == ''
- avatarMarkup = ''
- else
- 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/protected_branches.js.coffee b/app/assets/javascripts/protected_branches.js.coffee
index 691fd4f10d8..5753c9d4e72 100644
--- a/app/assets/javascripts/protected_branches.js.coffee
+++ b/app/assets/javascripts/protected_branches.js.coffee
@@ -1,5 +1,5 @@
$ ->
- $(":checkbox").change ->
+ $(".protected-branches-list :checkbox").change (e) ->
name = $(this).attr("name")
if name == "developers_can_push"
id = $(this).val()
@@ -14,8 +14,8 @@ $ ->
developers_can_push: checked
success: ->
- new Flash("Branch updated.", "notice")
- location.reload true
+ row = $(e.target)
+ row.closest('tr').effect('highlight')
error: ->
new Flash("Failed to update branch!", "alert")
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_issuable.coffee b/app/assets/javascripts/shortcuts_issuable.coffee
new file mode 100644
index 00000000000..6b534f29218
--- /dev/null
+++ b/app/assets/javascripts/shortcuts_issuable.coffee
@@ -0,0 +1,48 @@
+#= require jquery
+#= require mousetrap
+
+#= require shortcuts_navigation
+
+class @ShortcutsIssuable extends ShortcutsNavigation
+ constructor: (isMergeRequest) ->
+ super()
+ Mousetrap.bind('a', ->
+ $('.js-assignee').select2('open')
+ return false
+ )
+ Mousetrap.bind('m', ->
+ $('.js-milestone').select2('open')
+ return false
+ )
+ Mousetrap.bind('r', =>
+ @replyWithSelectedText()
+ return false
+ )
+
+ if isMergeRequest
+ @enabledHelp.push('.hidden-shortcut.merge_requests')
+ else
+ @enabledHelp.push('.hidden-shortcut.issues')
+
+ replyWithSelectedText: ->
+ if window.getSelection
+ selected = window.getSelection().toString()
+ replyField = $('.js-main-target-form #note_note')
+
+ return if selected.trim() == ""
+
+ # Put a '>' character before each non-empty line in the selection
+ quote = _.map selected.split("\n"), (val) ->
+ "> #{val}\n" if val.trim() != ''
+
+ # If replyField already has some content, add a newline before our quote
+ separator = replyField.val().trim() != "" and "\n" or ''
+
+ replyField.val (_, current) ->
+ current + separator + quote.join('') + "\n"
+
+ # Trigger autosave for the added text
+ replyField.trigger('input')
+
+ # Focus the input field
+ replyField.focus()
diff --git a/app/assets/javascripts/shortcuts_issueable.coffee b/app/assets/javascripts/shortcuts_issueable.coffee
deleted file mode 100644
index b8dae71e037..00000000000
--- a/app/assets/javascripts/shortcuts_issueable.coffee
+++ /dev/null
@@ -1,19 +0,0 @@
-#= require shortcuts_navigation
-
-class @ShortcutsIssueable extends ShortcutsNavigation
- constructor: (isMergeRequest) ->
- super()
- Mousetrap.bind('a', ->
- $('.js-assignee').select2('open')
- return false
- )
- Mousetrap.bind('m', ->
- $('.js-milestone').select2('open')
- return false
- )
-
- if isMergeRequest
- @enabledHelp.push('.hidden-shortcut.merge_reuests')
- else
- @enabledHelp.push('.hidden-shortcut.issues')
-
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..fb08016fbae 100644
--- a/app/assets/javascripts/sidebar.js.coffee
+++ b/app/assets/javascripts/sidebar.js.coffee
@@ -3,12 +3,8 @@ $(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}")
+ $('header').toggleClass("header-collapsed header-expanded")
+ $('.toggle-nav-collapse i').toggleClass("fa-angle-right fa-angle-left")
+ $.cookie("collapsed_nav", $('.page-with-sidebar').hasClass(collapsed), { path: '/' })
)
diff --git a/app/assets/javascripts/stat_graph_contributors.js.coffee b/app/assets/javascripts/stat_graph_contributors.js.coffee
index 27f0fd31d50..ed12bdcef22 100644
--- a/app/assets/javascripts/stat_graph_contributors.js.coffee
+++ b/app/assets/javascripts/stat_graph_contributors.js.coffee
@@ -1,3 +1,7 @@
+#= require d3
+#= require jquery
+#= require stat_graph_contributors_util
+
class @ContributorsStatGraph
init: (log) ->
@parsed_log = ContributorsStatGraphUtil.parse_log(log)
diff --git a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
index 8b82d20c6c2..0e6fbdef3bc 100644
--- a/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
+++ b/app/assets/javascripts/stat_graph_contributors_graph.js.coffee
@@ -1,3 +1,7 @@
+#= require d3
+#= require jquery
+#= require underscore
+
class @ContributorsGraph
MARGIN:
top: 20
diff --git a/app/assets/javascripts/subscription.js.coffee b/app/assets/javascripts/subscription.js.coffee
new file mode 100644
index 00000000000..7f41616d4e7
--- /dev/null
+++ b/app/assets/javascripts/subscription.js.coffee
@@ -0,0 +1,17 @@
+class @Subscription
+ constructor: (url) ->
+ $(".subscribe-button").unbind("click").click (event)=>
+ btn = $(event.currentTarget)
+ action = btn.find("span").text()
+ current_status = $(".subscription-status").attr("data-status")
+ btn.prop("disabled", true)
+
+ $.post url, =>
+ btn.prop("disabled", false)
+ status = if current_status == "subscribed" then "unsubscribed" else "subscribed"
+ $(".subscription-status").attr("data-status", status)
+ action = if status == "subscribed" then "Unsubscribe" else "Subscribe"
+ btn.find("span").text(action)
+ $(".subscription-status>div").toggleClass("hidden")
+
+
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/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss
index 0775c171817..21acbfa5e5a 100644
--- a/app/assets/stylesheets/base/gl_bootstrap.scss
+++ b/app/assets/stylesheets/base/gl_bootstrap.scss
@@ -117,7 +117,7 @@
color: #888;
text-shadow: 0 1px 1px #fff;
}
- i[class~="fa"] {
+ i.fa {
line-height: 14px;
}
}
@@ -137,6 +137,12 @@
color: #666;
}
+.nav-pills > .active > a > span > .badge {
+ background-color: #fff;
+ color: $gl-primary;
+}
+
+
/**
* fix to keep tooltips position in top navigation bar
*
@@ -152,12 +158,11 @@
*/
.panel {
.panel-heading {
- font-size: 14px;
- line-height: 18px;
+ font-weight: bold;
.panel-head-actions {
position: relative;
- top: -6px;
+ top: -5px;
float: right;
}
}
@@ -183,6 +188,7 @@
.panel-heading {
padding: 6px 15px;
font-size: 13px;
+ font-weight: normal;
a {
color: #777;
}
@@ -190,10 +196,62 @@
}
}
+.panel-succes .panel-heading,
+.panel-info .panel-heading,
+.panel-danger .panel-heading,
+.panel-warning .panel-heading,
+.panel-primary .panel-heading,
.alert {
- a {
+ a:not(.btn) {
@extend .alert-link;
color: #fff;
text-decoration: underline;
}
}
+
+// Typography =================================================================
+
+.text-primary,
+.text-primary:hover {
+ color: $brand-primary;
+}
+
+.text-success,
+.text-success:hover {
+ color: $brand-success;
+}
+
+.text-danger,
+.text-danger:hover {
+ color: $brand-danger;
+}
+
+.text-warning,
+.text-warning:hover {
+ color: $brand-warning;
+}
+
+.text-info,
+.text-info:hover {
+ color: $brand-info;
+}
+
+// Tables =====================================================================
+
+table.table {
+ .dropdown-menu a {
+ text-decoration: none;
+ }
+
+ .success,
+ .warning,
+ .danger,
+ .info {
+ color: #fff;
+
+ a:not(.btn) {
+ text-decoration: underline;
+ color: #fff;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss
index ea230646a89..56f4c794e1b 100644
--- a/app/assets/stylesheets/base/gl_variables.scss
+++ b/app/assets/stylesheets/base/gl_variables.scss
@@ -1,5 +1,5 @@
// Override Bootstrap variables here (defaults from bootstrap-sass v3.3.3):
-
+// For all variables see https://github.com/twbs/bootstrap-sass/blob/master/templates/project/_bootstrap-variables.sass
//
// Variables
// --------------------------------------------------
@@ -15,12 +15,6 @@
// $gray: lighten($gray-base, 33.5%) // #555
// $gray-light: lighten($gray-base, 46.7%) // #777
// $gray-lighter: lighten($gray-base, 93.5%) // #eee
-$gray-base: #000;
-$gray-darker: lighten($gray-base, 13.5%); // #222
-$gray-dark: #7b8a8b; // #333
-$gray: #95a5a6; // #555
-$gray-light: #b4bcc2; // #999
-$gray-lighter: #ecf0f1; // #eee
$brand-primary: $gl-primary;
$brand-success: $gl-success;
@@ -31,68 +25,17 @@ $brand-danger: $gl-danger;
//== Scaffolding
//
-//## Settings for some of the most global styles.
-
-//** Background color for `<body>`.
-// $body-bg: #fff
-//** Global text color on `<body>`.
-$text-color: $brand-primary;
-
-//** Global textual link color.
+$text-color: $gl-text-color;
$link-color: $gl-link-color;
-//** Link hover color set via `darken()` function.
-// $link-hover-color: darken($link-color, 15%)
-//** Link hover decoration.
-// $link-hover-decoration: underline
//== Typography
//
//## Font, line-height, and color for body text, headings, and more.
-// $font-family-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif
-// $font-family-serif: Georgia, "Times New Roman", Times, serif
-//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
-// $font-family-monospace: Menlo, Monaco, Consolas, "Courier New", monospace
-// $font-family-base: $font-family-sans-serif
-
-$font-size-base: $gl-font-size;
-// $font-size-large: ceil(($font-size-base * 1.25)) // ~18px
-// $font-size-small: ceil(($font-size-base * 0.85)) // ~12px
-
-// $font-size-h1: floor(($font-size-base * 2.6)) // ~36px
-// $font-size-h2: floor(($font-size-base * 2.15)) // ~30px
-// $font-size-h3: ceil(($font-size-base * 1.7)) // ~24px
-// $font-size-h4: ceil(($font-size-base * 1.25)) // ~18px
-// $font-size-h5: $font-size-base
-// $font-size-h6: ceil(($font-size-base * 0.85)) // ~12px
-
-//** Unit-less `line-height` for use in components like buttons.
-// $line-height-base: 1.428571429 // 20/14
-//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
-// $line-height-computed: floor(($font-size-base * $line-height-base)) // ~20px
-
-//** By default, this inherits from the `<body>`.
-// $headings-font-family: inherit
-// $headings-font-weight: 500
-// $headings-line-height: 1.1
-// $headings-color: inherit
-
-
-//== Iconography
-//
-//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
-
-//** Load fonts from this directory.
-
-// [converter] If $bootstrap-sass-asset-helper if used, provide path relative to the assets load path.
-// [converter] This is because some asset helpers, such as Sprockets, do not work with file-relative paths.
-// $icon-font-path: if($bootstrap-sass-asset-helper, "bootstrap/", "../fonts/bootstrap/")
-
-//** File name for all font files.
-// $icon-font-name: "glyphicons-halflings-regular"
-//** Element ID within SVG icon file.
-// $icon-font-svg-id: "glyphicons_halflingsregular"
+$font-family-sans-serif: $regular_font;
+$font-family-monospace: $monospace_font;
+$font-size-base: $gl-font-size;
//== Components
@@ -102,348 +45,15 @@ $font-size-base: $gl-font-size;
$padding-base-vertical: 6px;
$padding-base-horizontal: 14px;
-// $padding-large-vertical: 10px
-// $padding-large-horizontal: 16px
-
-// $padding-small-vertical: 5px
-// $padding-small-horizontal: 10px
-
-// $padding-xs-vertical: 1px
-// $padding-xs-horizontal: 5px
-
-// $line-height-large: 1.3333333 // extra decimals for Win 8.1 Chrome
-// $line-height-small: 1.5
-
-// $border-radius-base: 4px
-// $border-radius-large: 6px
-// $border-radius-small: 3px
-
-//** Global color for active items (e.g., navs or dropdowns).
-// $component-active-color: #fff
-//** Global background color for active items (e.g., navs or dropdowns).
-// $component-active-bg: $brand-primary
-
-//** Width of the `border` for generating carets that indicator dropdowns.
-// $caret-width-base: 4px
-//** Carets increase slightly in size for larger components.
-// $caret-width-large: 5px
-
-
-//== Tables
-//
-//## Customizes the `.table` component with basic values, each used across all table variations.
-
-//** Padding for `<th>`s and `<td>`s.
-// $table-cell-padding: 8px
-//** Padding for cells in `.table-condensed`.
-// $table-condensed-cell-padding: 5px
-
-//** Default background color used for all tables.
-// $table-bg: transparent
-//** Background color used for `.table-striped`.
-// $table-bg-accent: #f9f9f9
-//** Background color used for `.table-hover`.
-// $table-bg-hover: #f5f5f5
-// $table-bg-active: $table-bg-hover
-
-//** Border color for table and cell borders.
-// $table-border-color: #ddd
-
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-// $btn-font-weight: normal
-
-// $btn-default-color: #333
-// $btn-default-bg: #fff
-// $btn-default-border: #ccc
-
-// $btn-primary-color: #fff
-// $btn-primary-bg: $brand-primary
-// $btn-primary-border: darken($btn-primary-bg, 5%)
-
-// $btn-success-color: #fff
-// $btn-success-bg: $brand-success
-// $btn-success-border: darken($btn-success-bg, 5%)
-
-// $btn-info-color: #fff
-// $btn-info-bg: $brand-info
-// $btn-info-border: darken($btn-info-bg, 5%)
-
-// $btn-warning-color: #fff
-// $btn-warning-bg: $brand-warning
-// $btn-warning-border: darken($btn-warning-bg, 5%)
-
-// $btn-danger-color: #fff
-// $btn-danger-bg: $brand-danger
-// $btn-danger-border: darken($btn-danger-bg, 5%)
-
-// $btn-link-disabled-color: $gray-light
-
//== Forms
//
//##
-//** `<input>` background color
-// $input-bg: #fff
-//** `<input disabled>` background color
-// $input-bg-disabled: $gray-lighter
-
-//** Text color for `<input>`s
$input-color: $text-color;
-//** `<input>` border color
-$input-border: #dce4ec;
-
-// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
-//** Default `.form-control` border radius
-// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
-// $input-border-radius: $border-radius-base
-//** Large `.form-control` border radius
-// $input-border-radius-large: $border-radius-large
-//** Small `.form-control` border radius
-// $input-border-radius-small: $border-radius-small
-
-//** Border color for inputs on focus
+$input-border: #DDD;
$input-border-focus: $brand-info;
-
-//** Placeholder text color
-// $input-color-placeholder: #999
-
-//** Default `.form-control` height
-// $input-height-base: ($line-height-computed + ($padding-base-vertical * 2) + 2)
-//** Large `.form-control` height
-// $input-height-large: (ceil($font-size-large * $line-height-large) + ($padding-large-vertical * 2) + 2)
-//** Small `.form-control` height
-// $input-height-small: (floor($font-size-small * $line-height-small) + ($padding-small-vertical * 2) + 2)
-
$legend-color: $text-color;
-// $legend-border-color: #e5e5e5
-
-//** Background color for textual input addons
-// $input-group-addon-bg: $gray-lighter
-//** Border color for textual input addons
-// $input-group-addon-border-color: $input-border
-
-//** Disabled cursor for form controls and buttons.
-// $cursor-disabled: not-allowed
-
-
-//== Dropdowns
-//
-//## Dropdown menu container and contents.
-
-//** Background for the dropdown menu.
-// $dropdown-bg: #fff
-//** Dropdown menu `border-color`.
-// $dropdown-border: rgba(0,0,0,.15)
-//** Dropdown menu `border-color` **for IE8**.
-// $dropdown-fallback-border: #ccc
-//** Divider color for between dropdown items.
-// $dropdown-divider-bg: #e5e5e5
-
-//** Dropdown link text color.
-// $dropdown-link-color: $gray-dark
-//** Hover color for dropdown links.
-// $dropdown-link-hover-color: darken($gray-dark, 5%)
-//** Hover background for dropdown links.
-// $dropdown-link-hover-bg: #f5f5f5
-
-//** Active dropdown menu item text color.
-// $dropdown-link-active-color: $component-active-color
-//** Active dropdown menu item background color.
-// $dropdown-link-active-bg: $component-active-bg
-
-//** Disabled dropdown menu item background color.
-// $dropdown-link-disabled-color: $gray-light
-
-//** Text color for headers within dropdown menus.
-// $dropdown-header-color: $gray-light
-
-//** Deprecated `$dropdown-caret-color` as of v3.1.0
-// $dropdown-caret-color: #000
-
-
-//-- Z-index master list
-//
-// Warning: Avoid customizing these values. They're used for a bird's eye view
-// of components dependent on the z-axis and are designed to all work together.
-//
-// Note: These variables are not generated into the Customizer.
-
-// $zindex-navbar: 1000
-// $zindex-dropdown: 1000
-// $zindex-popover: 1060
-// $zindex-tooltip: 1070
-// $zindex-navbar-fixed: 1030
-// $zindex-modal: 1040
-
-
-//== Media queries breakpoints
-//
-//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
-
-// Extra small screen / phone
-//** Deprecated `$screen-xs` as of v3.0.1
-// $screen-xs: 480px
-//** Deprecated `$screen-xs-min` as of v3.2.0
-// $screen-xs-min: $screen-xs
-//** Deprecated `$screen-phone` as of v3.0.1
-// $screen-phone: $screen-xs-min
-
-// Small screen / tablet
-//** Deprecated `$screen-sm` as of v3.0.1
-// $screen-sm: 768px
-// $screen-sm-min: $screen-sm
-//** Deprecated `$screen-tablet` as of v3.0.1
-// $screen-tablet: $screen-sm-min
-
-// Medium screen / desktop
-//** Deprecated `$screen-md` as of v3.0.1
-// $screen-md: 992px
-// $screen-md-min: $screen-md
-//** Deprecated `$screen-desktop` as of v3.0.1
-// $screen-desktop: $screen-md-min
-
-// Large screen / wide desktop
-//** Deprecated `$screen-lg` as of v3.0.1
-// $screen-lg: 1200px
-// $screen-lg-min: $screen-lg
-//** Deprecated `$screen-lg-desktop` as of v3.0.1
-// $screen-lg-desktop: $screen-lg-min
-
-// So media queries don't overlap when required, provide a maximum
-// $screen-xs-max: ($screen-sm-min - 1)
-// $screen-sm-max: ($screen-md-min - 1)
-// $screen-md-max: ($screen-lg-min - 1)
-
-
-//== Grid system
-//
-//## Define your custom responsive grid.
-
-//** Number of columns in the grid.
-// $grid-columns: 12
-//** Padding between columns. Gets divided in half for the left and right.
-// $grid-gutter-width: 30px
-// Navbar collapse
-//** Point at which the navbar becomes uncollapsed.
-// $grid-float-breakpoint: $screen-sm-min
-//** Point at which the navbar begins collapsing.
-// $grid-float-breakpoint-max: ($grid-float-breakpoint - 1)
-
-
-//== Container sizes
-//
-//## Define the maximum width of `.container` for different screen sizes.
-
-// Small screen / tablet
-// $container-tablet: (720px + $grid-gutter-width)
-//** For `$screen-sm-min` and up.
-// $container-sm: $container-tablet
-
-// Medium screen / desktop
-// $container-desktop: (940px + $grid-gutter-width)
-//** For `$screen-md-min` and up.
-// $container-md: $container-desktop
-
-// Large screen / wide desktop
-// $container-large-desktop: (1140px + $grid-gutter-width)
-//** For `$screen-lg-min` and up.
-// $container-lg: $container-large-desktop
-
-
-//== Navbar
-//
-//##
-
-// Basics of a navbar
-// $navbar-height: 50px
-// $navbar-margin-bottom: $line-height-computed
-// $navbar-border-radius: $border-radius-base
-// $navbar-padding-horizontal: floor(($grid-gutter-width / 2))
-// $navbar-padding-vertical: (($navbar-height - $line-height-computed) / 2)
-// $navbar-collapse-max-height: 340px
-
-// $navbar-default-color: #777
-// $navbar-default-bg: #f8f8f8
-// $navbar-default-border: darken($navbar-default-bg, 6.5%)
-
-// Navbar links
-// $navbar-default-link-color: #777
-// $navbar-default-link-hover-color: #333
-// $navbar-default-link-hover-bg: transparent
-// $navbar-default-link-active-color: #555
-// $navbar-default-link-active-bg: darken($navbar-default-bg, 6.5%)
-// $navbar-default-link-disabled-color: #ccc
-// $navbar-default-link-disabled-bg: transparent
-
-// Navbar brand label
-// $navbar-default-brand-color: $navbar-default-link-color
-// $navbar-default-brand-hover-color: darken($navbar-default-brand-color, 10%)
-// $navbar-default-brand-hover-bg: transparent
-
-// Navbar toggle
-// $navbar-default-toggle-hover-bg: #ddd
-// $navbar-default-toggle-icon-bar-bg: #888
-// $navbar-default-toggle-border-color: #ddd
-
-
-// Inverted navbar
-// Reset inverted navbar basics
-// $navbar-inverse-color: lighten($gray-light, 15%)
-// $navbar-inverse-bg: #222
-// $navbar-inverse-border: darken($navbar-inverse-bg, 10%)
-
-// Inverted navbar links
-// $navbar-inverse-link-color: lighten($gray-light, 15%)
-// $navbar-inverse-link-hover-color: #fff
-// $navbar-inverse-link-hover-bg: transparent
-// $navbar-inverse-link-active-color: $navbar-inverse-link-hover-color
-// $navbar-inverse-link-active-bg: darken($navbar-inverse-bg, 10%)
-// $navbar-inverse-link-disabled-color: #444
-// $navbar-inverse-link-disabled-bg: transparent
-
-// Inverted navbar brand label
-// $navbar-inverse-brand-color: $navbar-inverse-link-color
-// $navbar-inverse-brand-hover-color: #fff
-// $navbar-inverse-brand-hover-bg: transparent
-
-// Inverted navbar toggle
-// $navbar-inverse-toggle-hover-bg: #333
-// $navbar-inverse-toggle-icon-bar-bg: #fff
-// $navbar-inverse-toggle-border-color: #333
-
-
-//== Navs
-//
-//##
-
-//=== Shared nav styles
-// $nav-link-padding: 10px 15px
-// $nav-link-hover-bg: $gray-lighter
-
-// $nav-disabled-link-color: $gray-light
-// $nav-disabled-link-hover-color: $gray-light
-
-//== Tabs
-// $nav-tabs-border-color: #ddd
-
-// $nav-tabs-link-hover-border-color: $gray-lighter
-
-// $nav-tabs-active-link-hover-bg: $body-bg
-// $nav-tabs-active-link-hover-color: $gray
-// $nav-tabs-active-link-hover-border-color: #ddd
-
-// $nav-tabs-justified-link-border-color: #ddd
-// $nav-tabs-justified-active-link-border-color: $body-bg
-
-//== Pills
-// $nav-pills-border-radius: $border-radius-base
-// $nav-pills-active-link-hover-bg: $component-active-bg
-// $nav-pills-active-link-hover-color: $component-active-color
//== Pagination
@@ -467,38 +77,10 @@ $pagination-disabled-bg: lighten($brand-success, 15%);
$pagination-disabled-border: transparent;
-//== Pager
-//
-//##
-
-// $pager-bg: $pagination-bg
-// $pager-border: $pagination-border
-// $pager-border-radius: 15px
-
-// $pager-hover-bg: $pagination-hover-bg
-
-// $pager-active-bg: $pagination-active-bg
-// $pager-active-color: $pagination-active-color
-
-// $pager-disabled-color: $pagination-disabled-color
-
-
-//== Jumbotron
-//
-//##
-
-// $jumbotron-padding: 30px
-// $jumbotron-color: inherit
-// $jumbotron-bg: $gray-lighter
-// $jumbotron-heading-color: inherit
-// $jumbotron-font-size: ceil(($font-size-base * 1.5))
-
-
//== Form states and alerts
//
//## Define colors for form feedback states and, by default, alerts.
-
$state-success-text: #fff;
$state-success-bg: $brand-success;
$state-success-border: $brand-success;
@@ -516,317 +98,29 @@ $state-danger-bg: $brand-danger;
$state-danger-border: $brand-danger;
-//== Tooltips
-//
-//##
-
-//** Tooltip max width
-// $tooltip-max-width: 200px
-//** Tooltip text color
-// $tooltip-color: #fff
-//** Tooltip background color
-// $tooltip-bg: #000
-// $tooltip-opacity: .9
-
-//** Tooltip arrow width
-// $tooltip-arrow-width: 5px
-//** Tooltip arrow color
-// $tooltip-arrow-color: $tooltip-bg
-
-
-//== Popovers
-//
-//##
-
-//** Popover body background color
-// $popover-bg: #fff
-//** Popover maximum width
-// $popover-max-width: 276px
-//** Popover border color
-// $popover-border-color: rgba(0,0,0,.2)
-//** Popover fallback border color
-// $popover-fallback-border-color: #ccc
-
-//** Popover title background color
-// $popover-title-bg: darken($popover-bg, 3%)
-
-//** Popover arrow width
-// $popover-arrow-width: 10px
-//** Popover arrow color
-// $popover-arrow-color: $popover-bg
-
-//** Popover outer arrow width
-// $popover-arrow-outer-width: ($popover-arrow-width + 1)
-//** Popover outer arrow color
-// $popover-arrow-outer-color: fade_in($popover-border-color, 0.05)
-//** Popover outer arrow fallback color
-// $popover-arrow-outer-fallback-color: darken($popover-fallback-border-color, 20%)
-
-
-//== Labels
-//
-//##
-
-//** Default label background color
-// $label-default-bg: $gray-light
-//** Primary label background color
-// $label-primary-bg: $brand-primary
-//** Success label background color
-// $label-success-bg: $brand-success
-//** Info label background color
-// $label-info-bg: $brand-info
-//** Warning label background color
-// $label-warning-bg: $brand-warning
-//** Danger label background color
-// $label-danger-bg: $brand-danger
-
-//** Default label text color
-// $label-color: #fff
-//** Default text color of a linked label
-// $label-link-hover-color: #fff
-
-
-//== Modals
-//
-//##
-
-//** Padding applied to the modal body
-// $modal-inner-padding: 15px
-
-//** Padding applied to the modal title
-// $modal-title-padding: 15px
-//** Modal title line-height
-// $modal-title-line-height: $line-height-base
-
-//** Background color of modal content area
-// $modal-content-bg: #fff
-//** Modal content border color
-// $modal-content-border-color: rgba(0,0,0,.2)
-//** Modal content border color **for IE8**
-// $modal-content-fallback-border-color: #999
-
-//** Modal backdrop background color
-// $modal-backdrop-bg: #000
-//** Modal backdrop opacity
-// $modal-backdrop-opacity: .5
-//** Modal header border color
-// $modal-header-border-color: #e5e5e5
-//** Modal footer border color
-// $modal-footer-border-color: $modal-header-border-color
-
-// $modal-lg: 900px
-// $modal-md: 600px
-// $modal-sm: 300px
-
-
//== Alerts
//
//## Define alert colors, border radius, and padding.
-// $alert-padding: 15px
$alert-border-radius: 0;
-// $alert-link-font-weight: bold
-
-// $alert-success-bg: $state-success-bg
-// $alert-success-text: $state-success-text
-// $alert-success-border: $state-success-border
-
-// $alert-info-bg: $state-info-bg
-// $alert-info-text: $state-info-text
-// $alert-info-border: $state-info-border
-
-// $alert-warning-bg: $state-warning-bg
-// $alert-warning-text: $state-warning-text
-// $alert-warning-border: $state-warning-border
-
-// $alert-danger-bg: $state-danger-bg
-// $alert-danger-text: $state-danger-text
-// $alert-danger-border: $state-danger-border
-
-
-//== Progress bars
-//
-//##
-
-//** Background color of the whole progress component
-// $progress-bg: #f5f5f5
-//** Progress bar text color
-// $progress-bar-color: #fff
-//** Variable for setting rounded corners on progress bar.
-// $progress-border-radius: $border-radius-base
-
-//** Default progress bar color
-// $progress-bar-bg: $brand-primary
-//** Success progress bar color
-// $progress-bar-success-bg: $brand-success
-//** Warning progress bar color
-// $progress-bar-warning-bg: $brand-warning
-//** Danger progress bar color
-// $progress-bar-danger-bg: $brand-danger
-//** Info progress bar color
-// $progress-bar-info-bg: $brand-info
-
-
-//== List group
-//
-//##
-
-//** Background color on `.list-group-item`
-// $list-group-bg: #fff
-//** `.list-group-item` border color
-// $list-group-border: #ddd
-//** List group border radius
-// $list-group-border-radius: $border-radius-base
-
-//** Background color of single list items on hover
-// $list-group-hover-bg: #f5f5f5
-//** Text color of active list items
-// $list-group-active-color: $component-active-color
-//** Background color of active list items
-// $list-group-active-bg: $component-active-bg
-//** Border color of active list elements
-// $list-group-active-border: $list-group-active-bg
-//** Text color for content within active list items
-// $list-group-active-text-color: lighten($list-group-active-bg, 40%)
-
-//** Text color of disabled list items
-// $list-group-disabled-color: $gray-light
-//** Background color of disabled list items
-// $list-group-disabled-bg: $gray-lighter
-//** Text color for content within disabled list items
-// $list-group-disabled-text-color: $list-group-disabled-color
-
-// $list-group-link-color: #555
-// $list-group-link-hover-color: $list-group-link-color
-// $list-group-link-heading-color: #333
//== Panels
//
//##
-// $panel-bg: #fff
-// $panel-body-padding: 15px
-// $panel-heading-padding: 10px 15px
-// $panel-footer-padding: $panel-heading-padding
$panel-border-radius: 0;
-
-//** Border color for elements within panels
-// $panel-inner-border: #ddd
-// $panel-footer-bg: #f5f5f5
-
$panel-default-text: $text-color;
-// $panel-default-border: #ddd
-// $panel-default-heading-bg: #f5f5f5
-
-// $panel-primary-text: #fff
-// $panel-primary-border: $brand-primary
-// $panel-primary-heading-bg: $brand-primary
-
-// $panel-success-text: $state-success-text
-// $panel-success-border: $state-success-border
-// $panel-success-heading-bg: $state-success-bg
-
-// $panel-info-text: $state-info-text
-// $panel-info-border: $state-info-border
-// $panel-info-heading-bg: $state-info-bg
-
-// $panel-warning-text: $state-warning-text
-// $panel-warning-border: $state-warning-border
-// $panel-warning-heading-bg: $state-warning-bg
-
-// $panel-danger-text: $state-danger-text
-// $panel-danger-border: $state-danger-border
-// $panel-danger-heading-bg: $state-danger-bg
-
-
-//== Thumbnails
-//
-//##
-
-//** Padding around the thumbnail image
-// $thumbnail-padding: 4px
-//** Thumbnail background color
-// $thumbnail-bg: $body-bg
-//** Thumbnail border color
-// $thumbnail-border: #ddd
-//** Thumbnail border radius
-// $thumbnail-border-radius: $border-radius-base
-
-//** Custom text color for thumbnail captions
-// $thumbnail-caption-color: $text-color
-//** Padding around the thumbnail caption
-// $thumbnail-caption-padding: 9px
+$panel-default-border: $border-color;
+$panel-default-heading-bg: $background-color;
//== Wells
//
//##
-$well-bg: $gray-lighter;
-$well-border: transparent;
-
-
-//== Badges
-//
-//##
-
-// $badge-color: #fff
-//** Linked badge text color on hover
-// $badge-link-hover-color: #fff
-// $badge-bg: $gray-light
-
-//** Badge text color in active nav link
-// $badge-active-color: $link-color
-//** Badge background color in active nav link
-// $badge-active-bg: #fff
-
-// $badge-font-weight: bold
-// $badge-line-height: 1
-// $badge-border-radius: 10px
-
-
-//== Breadcrumbs
-//
-//##
-
-// $breadcrumb-padding-vertical: 8px
-// $breadcrumb-padding-horizontal: 15px
-//** Breadcrumb background color
-// $breadcrumb-bg: #f5f5f5
-//** Breadcrumb text color
-// $breadcrumb-color: #ccc
-//** Text color of current page in the breadcrumb
-// $breadcrumb-active-color: $gray-light
-//** Textual separator for between breadcrumb elements
-// $breadcrumb-separator: "/"
-
-
-//== Carousel
-//
-//##
-
-// $carousel-text-shadow: 0 1px 2px rgba(0,0,0,.6)
-
-// $carousel-control-color: #fff
-// $carousel-control-width: 15%
-// $carousel-control-opacity: .5
-// $carousel-control-font-size: 20px
-
-// $carousel-indicator-active-bg: #fff
-// $carousel-indicator-border-color: #fff
-
-// $carousel-caption-color: #fff
-
-
-//== Close
-//
-//##
-
-// $close-font-weight: bold
-// $close-color: #000
-// $close-text-shadow: 0 1px 0 #fff
-
+$well-bg: #F9F9F9;
+$well-border: #EEE;
//== Code
//
@@ -837,34 +131,3 @@ $code-bg: #f9f2f4;
$kbd-color: #fff;
$kbd-bg: #333;
-
-$pre-bg: $gray-lighter;
-$pre-color: $text-color;
-$pre-border-color: #ccc;
-// $pre-scrollable-max-height: 340px
-
-
-//== Type
-//
-//##
-
-//** Horizontal offset for forms and lists.
-// $component-offset-horizontal: 180px
-//** Text muted color
-// $text-muted: $gray-light
-//** Abbreviations and acronyms border color
-// $abbr-border-color: $gray-light
-//** Headings small color
-$headings-small-color: $gray-dark;
-//** Blockquote small color
-// $blockquote-small-color: $gray-light
-//** Blockquote font size
-// $blockquote-font-size: ($font-size-base * 1.25)
-//** Blockquote border color
-// $blockquote-border-color: $gray-lighter
-//** Page header border color
-// $page-header-border-color: $gray-lighter
-//** Width of horizontal description list titles
-// $dl-horizontal-offset: $component-offset-horizontal
-//** Horizontal line color.
-// $hr-border: $gray-lighter
diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss
index ccba65e3fd5..a0794e7825a 100644
--- a/app/assets/stylesheets/base/mixins.scss
+++ b/app/assets/stylesheets/base/mixins.scss
@@ -21,6 +21,10 @@
@include border-radius($radius 0 0 $radius)
}
+@mixin border-radius-right($radius) {
+ @include border-radius(0 0 $radius $radius)
+}
+
@mixin linear-gradient($from, $to) {
background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
background-image: -webkit-linear-gradient($from, $to);
@@ -50,13 +54,6 @@
@include box-shadow(0 0 0 3px #f1f1f1);
}
-@mixin header-font {
- color: $style_color;
- font-size: 16px;
- line-height: 44px;
- font-weight: normal;
-}
-
@mixin md-typography {
font-size: 15px;
line-height: 1.5;
@@ -113,12 +110,27 @@
p > code {
font-size: inherit;
font-weight: inherit;
- color: #555;
}
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/base/variables.scss b/app/assets/stylesheets/base/variables.scss
index 54af78ee082..596376c3970 100644
--- a/app/assets/stylesheets/base/variables.scss
+++ b/app/assets/stylesheets/base/variables.scss
@@ -1,6 +1,6 @@
$style_color: #474D57;
$hover: #FFF3EB;
-$box_bg: #F9F9F9;
+$gl-text-color: #222222;
$gl-link-color: #446e9b;
$nprogress-color: #c0392b;
$gl-font-size: 14px;
@@ -9,21 +9,18 @@ $sidebar_width: 230px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
+$border-color: #E5E5E5;
+$background-color: #f5f5f5;
/*
* State colors:
*/
-$gl-success: #019875;
-$gl-danger: #d9534f;
$gl-primary: #446e9b;
+$gl-success: #019875;
$gl-info: #029ACF;
$gl-warning: #EB9532;
+$gl-danger: #d9534f;
-$gl-primary: #2C3E50;
-$gl-success: #18BC9C;
-$gl-info: #3498DB;
-$gl-warning: #F39C12;
-$gl-danger: #E74C3C;
/*
* Commit Diff Colors
*/
diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss
index 0224484d82b..cd6bf64c0ae 100644
--- a/app/assets/stylesheets/generic/buttons.scss
+++ b/app/assets/stylesheets/generic/buttons.scss
@@ -21,18 +21,6 @@
float: right;
}
- &.btn-small {
- padding: 2px 10px;
- font-size: 12px;
- }
-
- &.btn-tiny {
- font-size: 11px;
- padding: 2px 6px;
- line-height: 16px;
- margin: 2px;
- }
-
&.btn-close {
color: $gl-danger;
border-color: $gl-danger;
@@ -84,6 +72,3 @@
}
}
}
-
-.btn-group-small > .btn { @extend .btn.btn-small; }
-.btn-group-tiny > .btn { @extend .btn.btn-tiny; }
diff --git a/app/assets/stylesheets/generic/calendar.scss b/app/assets/stylesheets/generic/calendar.scss
index 9483b26164e..a36fefe22c5 100644
--- a/app/assets/stylesheets/generic/calendar.scss
+++ b/app/assets/stylesheets/generic/calendar.scss
@@ -1,29 +1,24 @@
-.calendar_onclick_placeholder {
- padding: 0 0 2px 0;
-}
-
-.calendar_commit_activity {
- padding: 5px 0 0;
-}
-
-.calendar_onclick_second {
- font-size: 14px;
- display: block;
-}
-
-.calendar_onclick_hr {
- padding: 0;
- margin: 10px 0;
-}
+.user-calendar-activities {
+ .calendar_onclick_hr {
+ padding: 0;
+ margin: 10px 0;
+ }
-.calendar_commit_date {
- color: #999;
-}
+ .str-truncated {
+ max-width: 70%;
+ }
-.calendar_activity_summary {
- font-size: 14px;
+ .text-expander {
+ background: #eee;
+ color: #555;
+ padding: 0 5px;
+ cursor: pointer;
+ margin-left: 4px;
+ &:hover {
+ background-color: #ddd;
+ }
+ }
}
-
/**
* This overwrites the default values of the cal-heatmap gem
*/
diff --git a/app/assets/stylesheets/generic/common.scss b/app/assets/stylesheets/generic/common.scss
index af8e90eb1a9..1e569978cc8 100644
--- a/app/assets/stylesheets/generic/common.scss
+++ b/app/assets/stylesheets/generic/common.scss
@@ -167,7 +167,7 @@ li.note {
background-color: inherit;
}
-.team_member_show {
+.project_member_show {
td:first-child {
color: #aaa;
}
@@ -246,7 +246,7 @@ li.note {
.milestone {
&.milestone-closed {
- background: #eee;
+ background: #f9f9f9;
}
.progress {
margin-bottom: 0;
@@ -333,17 +333,8 @@ table {
}
.search_box {
- position: relative;
- padding: 30px;
+ @extend .well;
text-align: center;
- background-color: #F9F9F9;
- border: 1px solid #DDDDDD;
- border-radius: 0px;
-}
-
-.search_glyph {
- color: #555;
- font-size: 42px;
}
.task-status {
@@ -355,3 +346,22 @@ table {
bottom: 20px !important;
left: 20px !important;
}
+
+.header-with-avatar {
+ h3 {
+ margin: 0;
+ font-weight: bold;
+ }
+
+ .username {
+ font-size: 18px;
+ color: #666;
+ margin-top: 8px;
+ }
+
+ .description {
+ font-size: 16px;
+ color: #666;
+ margin-top: 8px;
+ }
+}
diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss
index 1ed41272ac5..8014dcb165b 100644
--- a/app/assets/stylesheets/generic/files.scss
+++ b/app/assets/stylesheets/generic/files.scss
@@ -3,7 +3,7 @@
*
*/
.file-holder {
- border: 1px solid #CCC;
+ border: 1px solid $border-color;
margin-bottom: 1em;
table {
@@ -11,34 +11,30 @@
}
.file-title {
- background: #EEE;
- border-bottom: 1px solid #CCC;
+ position: relative;
+ background: $background-color;
+ border-bottom: 1px solid $border-color;
text-shadow: 0 1px 1px #fff;
margin: 0;
text-align: left;
padding: 10px 15px;
- .options {
+ .file-actions {
float: right;
- margin-top: -3px;
+ position: absolute;
+ top: 5px;
+ right: 15px;
+
+ .btn {
+ padding: 0px 10px;
+ font-size: 13px;
+ line-height: 28px;
+ }
}
.left-options {
margin-top: -3px;
}
-
- .file_name {
- font-weight: bold;
- padding-left: 3px;
- font-size: 14px;
-
- small {
- color: #888;
- font-size: 13px;
- font-weight: normal;
- padding-left: 10px;
- }
- }
}
.file-content {
background: #fff;
@@ -98,7 +94,7 @@
}
.author,
.blame_commit {
- background: #f5f5f5;
+ background: $background-color;
vertical-align: top;
}
.lines {
@@ -119,7 +115,7 @@
ol {
margin-left: 40px;
padding: 10px 0;
- border-left: 1px solid #CCC;
+ border-left: 1px solid $border-color;
margin-bottom: 0;
background: white;
li {
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/forms.scss b/app/assets/stylesheets/generic/forms.scss
index 19bc11086e9..266041403e0 100644
--- a/app/assets/stylesheets/generic/forms.scss
+++ b/app/assets/stylesheets/generic/forms.scss
@@ -15,10 +15,6 @@ input[type='text'].danger {
text-shadow: 0 1px 1px #fff
}
-fieldset legend {
- font-size: 16px;
-}
-
.datetime-controls {
select {
width: 100px;
@@ -29,8 +25,8 @@ fieldset legend {
padding: 17px 20px 18px;
margin-top: 18px;
margin-bottom: 18px;
- background-color: #ecf0f1;
- border-top: 1px solid #e5e5e5;
+ background-color: $background-color;
+ border-top: 1px solid $border-color;
}
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/pages/header.scss b/app/assets/stylesheets/generic/header.scss
index 26b4d04106e..fcd62373bfd 100644
--- a/app/assets/stylesheets/pages/header.scss
+++ b/app/assets/stylesheets/generic/header.scss
@@ -10,10 +10,59 @@ header {
border: none;
width: 100%;
- .navbar-inner {
+ .container {
+ width: 100% !important;
+ padding: 0;
+
+ background: #FFF;
+ border-bottom: 1px solid #EEE;
filter: none;
+ .title {
+ position: relative;
+ float: left;
+ margin: 0;
+ margin-left: 25px;
+ font-size: 18px;
+ line-height: 44px;
+ font-weight: bold;
+ color: #444;
+
+ @include str-truncated(37%);
+
+ a {
+ color: #444;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .app_logo {
+ border-bottom: 1px solid transparent;
+ margin-bottom: -1px;
+
+ a {
+ padding: 5px 8px;
+
+ img {
+ float: left;
+ }
+
+ h3 {
+ width: 158px;
+ float: left;
+ margin: 0;
+ margin-left: 20px;
+ font-size: 18px;
+ line-height: 34px;
+ font-weight: normal;
+ }
+ }
+ }
+
.nav > li > a {
+ color: #666;
font-size: 14px;
line-height: 32px;
padding: 6px 10px;
@@ -30,15 +79,10 @@ header {
}
.navbar-toggle {
- color: $style_color;
- margin: 0 -15px 0 0;
- padding: 10px;
+ color: #666;
+ margin: 0;
border-radius: 0;
- button i { font-size: 22px; }
-
- &.collapsed { background-color: transparent !important;}
-
&:hover {
background-color: #EEE;
}
@@ -60,8 +104,6 @@ header {
.navbar-collapse {
margin-top: 47px;
- padding-right: 0;
- padding-left: 0;
}
.navbar-nav {
@@ -84,11 +126,6 @@ header {
}
}
- .container {
- width: 100% !important;
- padding: 0px;
- }
-
/**
*
* Logo holder
@@ -100,10 +137,8 @@ header {
a {
float: left;
- padding: 5px 0;
height: 46px;
- width: 52px;
- text-align: center;
+ width: 100%;
img {
width: 36px;
@@ -115,20 +150,6 @@ header {
}
}
- /**
- *
- * Project / Area name
- *
- */
- .title {
- position: relative;
- float: left;
- margin: 0;
- margin-left: 5px;
- @include header-font;
- @include str-truncated(37%);
- }
-
.profile-pic {
padding: 0px !important;
width: 46px;
@@ -164,9 +185,10 @@ header {
padding-left: 25px;
font-size: 13px;
@include border-radius(3px);
- border: 1px solid #c6c6c6;
+ border: 1px solid #DDD;
box-shadow: none;
@include transition(all 0.15s ease-in 0s);
+ background-color: #f5f5f5;
}
}
}
@@ -192,3 +214,26 @@ header {
right: 35px !important;
}
}
+
+@media (max-width: $screen-md-max) {
+ .header-collapsed, .header-expanded {
+ width: 52px;
+
+ h3 {
+ display: none;
+ }
+ }
+}
+
+@media(min-width: $screen-md-max) {
+ .header-collapsed {
+ width: 52px;
+
+ h3 {
+ display: none;
+ }
+ }
+
+ .header-expanded {
+ }
+}
diff --git a/app/assets/stylesheets/generic/highlight.scss b/app/assets/stylesheets/generic/highlight.scss
index 0f8225d6823..2e13ee842e0 100644
--- a/app/assets/stylesheets/generic/highlight.scss
+++ b/app/assets/stylesheets/generic/highlight.scss
@@ -57,7 +57,7 @@
.note-text .code {
border: none;
box-shadow: none;
- background: $box_bg;
+ background: $background-color;
padding: 1em;
overflow-x: auto;
diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss
index 9558f241b7c..869e586839b 100644
--- a/app/assets/stylesheets/generic/issue_box.scss
+++ b/app/assets/stylesheets/generic/issue_box.scss
@@ -6,7 +6,7 @@
.issue-box {
display: inline-block;
- padding: 7px 13px;
+ padding: 4px 13px;
font-weight: normal;
margin-right: 5px;
diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss
index 5950885c42c..08bf6e943d2 100644
--- a/app/assets/stylesheets/generic/lists.scss
+++ b/app/assets/stylesheets/generic/lists.scss
@@ -35,7 +35,7 @@
color: #8a6d3b;
}
- &.smoke { background-color: #f5f5f5; }
+ &.smoke { background-color: $background-color; }
&:hover {
background: $hover;
@@ -46,7 +46,7 @@
border-bottom: none;
&.bottom {
- background: #f5f5f5;
+ background: $background-color;
}
}
@@ -61,7 +61,7 @@
p {
padding-top: 1px;
margin: 0;
- color: #222;
+ color: $gray-dark;
img {
position: relative;
top: 3px;
@@ -74,9 +74,10 @@
}
.row_title {
- color: #444;
+ color: $gray-dark;
+
&:hover {
- color: #444;
+ color: $text-color;
text-decoration: underline;
}
}
diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss
index 1b0e056216f..b7f6fac5223 100644
--- a/app/assets/stylesheets/generic/mobile.scss
+++ b/app/assets/stylesheets/generic/mobile.scss
@@ -4,6 +4,11 @@
margin-top: 20px;
}
+ .container-fluid {
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
.nav.nav-tabs > li > a {
padding: 10px;
font-size: 12px;
@@ -24,15 +29,36 @@
display: none !important;
}
+ .project-home-links {
+ display: none;
+ }
+
+ .project-avatar {
+ display: none;
+ }
+
.project-home-panel {
- .star-fork-buttons {
- padding-top: 10px;
- padding-right: 15px;
+ padding-left: 0 !important;
+
+ .project-home-row {
+ .project-home-desc {
+ margin-right: 0 !important;
+ float: none !important;
+ }
+
+ .project-repo-buttons {
+ position: static;
+ margin-top: 15px;
+ width: 100%;
+ float: none;
+ text-align: left;
+ }
}
}
- .project-home-links {
- display: none;
+ .container .title {
+ margin-left: 6px !important;
+ max-width: 70% !important;
}
}
diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss
index af0ecb192d6..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: #BBB;
+ 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;
@@ -46,55 +52,13 @@
}
}
-select {
- &.select2 {
- width: 100px;
- }
-
- &.select2-sm {
- width: 100px;
- }
-}
-
-@media (min-width: $screen-sm-min) {
- select {
- &.select2 {
- width: 150px;
- }
- &.select2-sm {
- width: 120px;
- }
- }
-}
-
-/* Medium devices (desktops, 992px and up) */
-@media (min-width: $screen-md-min) {
- select {
- &.select2 {
- width: 170px;
- }
- &.select2-sm {
- width: 140px;
- }
- }
-}
-
-/* Large devices (large desktops, 1200px and up) */
-@media (min-width: $screen-lg-min) {
- select {
- &.select2 {
- width: 200px;
- }
- &.select2-sm {
- width: 150px;
- }
- }
+.select2-container {
+ width: 100% !important;
}
-
/** Branch/tag selector **/
.project-refs-form .select2-container {
- margin-right: 10px;
+ width: 160px !important;
}
.ajax-users-dropdown, .ajax-project-users-dropdown {
@@ -148,3 +112,7 @@ select {
font-weight: bolder;
}
}
+
+.ajax-users-dropdown {
+ min-width: 225px !important;
+}
diff --git a/app/assets/stylesheets/generic/nav_sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss
index 4bf2c609be0..754c5b53020 100644
--- a/app/assets/stylesheets/generic/nav_sidebar.scss
+++ b/app/assets/stylesheets/generic/sidebar.scss
@@ -1,18 +1,17 @@
.page-with-sidebar {
- background: #F5F5F5;
+ background: $background-color;
.sidebar-wrapper {
position: fixed;
top: 0;
left: 0;
height: 100%;
- border-right: 1px solid #EAEAEA;
}
}
.sidebar-wrapper {
z-index: 99;
- background: #F5F5F5;
+ background: $background-color;
}
.content-wrapper {
@@ -38,46 +37,25 @@
}
.nav-sidebar li {
- &.active a {
- color: #333;
- background: #FFF !important;
- font-weight: bold;
- border: 1px solid #EEE;
- border-right: 1px solid transparent;
- border-left: 3px solid $style_color;
-
- &.no-highlight {
- background: none !important;
- border: none;
- }
-
- i {
- color: #444;
- }
- }
}
.nav-sidebar li {
&.separate-item {
- border-top: 1px solid #ddd;
padding-top: 10px;
margin-top: 10px;
}
a {
- color: #555;
+ color: $gray;
display: block;
text-decoration: none;
padding: 8px 15px;
font-size: 13px;
line-height: 20px;
- text-shadow: 0 1px 2px #FFF;
- padding-left: 20px;
+ padding-left: 16px;
&:hover {
text-decoration: none;
- color: #333;
- background: #EEE;
}
&:active, &:focus {
@@ -86,7 +64,7 @@
i {
width: 20px;
- color: #888;
+ color: $gray-light;
margin-right: 23px;
}
}
@@ -147,7 +125,7 @@
.collapse-nav a {
left: 0px;
- padding: 5px 23px 3px 22px;
+ width: 52px;
}
}
}
@@ -155,12 +133,18 @@
.collapse-nav a {
position: fixed;
top: 46px;
- padding: 5px 13px 3px 13px;
- left: 197px;
+ left: 198px;
font-size: 13px;
- background: #EEE;
- color: black;
- border: 1px solid rgba(0,0,0,0.035);
+ background: transparent;
+ width: 32px;
+ height: 28px;
+ text-align: center;
+ line-height: 28px;
+}
+
+.collapse-nav a:hover {
+ text-decoration: none;
+ background: #f2f6f7;
}
@media (max-width: $screen-md-max) {
diff --git a/app/assets/stylesheets/generic/tables.scss b/app/assets/stylesheets/generic/tables.scss
index 71a7d4abaee..a66e45577de 100644
--- a/app/assets/stylesheets/generic/tables.scss
+++ b/app/assets/stylesheets/generic/tables.scss
@@ -9,7 +9,7 @@ table {
th {
font-weight: normal;
font-size: 15px;
- border-bottom: 1px solid #CCC !important;
+ border-bottom: 1px solid $border-color !important;
}
td {
border-color: #F1F1F1 !important;
diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss
index f92a79f7a5f..97831eb7c27 100644
--- a/app/assets/stylesheets/generic/timeline.scss
+++ b/app/assets/stylesheets/generic/timeline.scss
@@ -54,7 +54,7 @@
.timeline-content {
position: relative;
- background: #f5f5f6;
+ background: $background-color;
padding: 10px 15px;
margin-left: 60px;
@@ -70,7 +70,7 @@
height: 0;
border-style: solid;
border-width: 9px 9px 9px 0;
- border-color: transparent #f5f5f6 transparent transparent;
+ border-color: transparent $background-color transparent transparent;
left: 0;
top: 10px;
margin-left: -9px;
diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss
index 4d940ee6b29..e5590897947 100644
--- a/app/assets/stylesheets/generic/typography.scss
+++ b/app/assets/stylesheets/generic/typography.scss
@@ -4,7 +4,6 @@
*/
.page-title {
margin-top: 0px;
- color: #333;
line-height: 1.5;
font-weight: normal;
margin-bottom: 5px;
@@ -16,7 +15,7 @@ pre {
&.dark {
background: #333;
- color: #f5f5f5;
+ color: $background-color;
}
}
@@ -36,7 +35,14 @@ pre {
/* Link to current header. */
h1, h2, h3, h4, h5, h6 {
position: relative;
- &:hover > :last-child {
+
+ a.anchor {
+ // Setting `display: none` would prevent the anchor being scrolled to, so
+ // instead we set the height to 0 and it gets updated on hover.
+ height: 0;
+ }
+
+ &:hover > a.anchor {
$size: 16px;
position: absolute;
right: 100%;
diff --git a/app/assets/stylesheets/highlight/dark.scss b/app/assets/stylesheets/highlight/dark.scss
index fcd4d47bace..c8cb18ec35f 100644
--- a/app/assets/stylesheets/highlight/dark.scss
+++ b/app/assets/stylesheets/highlight/dark.scss
@@ -1,6 +1,10 @@
/* https://github.com/MozMorris/tomorrow-pygments */
+pre.code.highlight.dark,
.code.dark {
+ background-color: #1d1f21;
+ color: #c5c8c6;
+
pre.code,
.line-numbers,
.line-numbers a {
@@ -13,8 +17,8 @@
}
// highlight line via anchor
- pre.hll {
- background-color: #fff !important;
+ pre .hll {
+ background-color: #557 !important;
}
.hll { background-color: #373b41 }
diff --git a/app/assets/stylesheets/highlight/monokai.scss b/app/assets/stylesheets/highlight/monokai.scss
index bcd2e716657..001e8b31020 100644
--- a/app/assets/stylesheets/highlight/monokai.scss
+++ b/app/assets/stylesheets/highlight/monokai.scss
@@ -1,6 +1,10 @@
/* https://github.com/richleland/pygments-css/blob/master/monokai.css */
+pre.code.monokai,
.code.monokai {
+ background: #272822;
+ color: #f8f8f2;
+
pre.highlight,
.line-numbers,
.line-numbers a {
@@ -13,7 +17,7 @@
}
// highlight line via anchor
- pre.hll {
+ pre .hll {
background-color: #49483e !important;
}
diff --git a/app/assets/stylesheets/highlight/solarized_dark.scss b/app/assets/stylesheets/highlight/solarized_dark.scss
index 4a6b759bd2c..f5b827e7c02 100644
--- a/app/assets/stylesheets/highlight/solarized_dark.scss
+++ b/app/assets/stylesheets/highlight/solarized_dark.scss
@@ -1,6 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
+pre.code.highlight.solarized-dark,
.code.solarized-dark {
+ background-color: #002b36;
+ color: #93a1a1;
+
pre.code,
.line-numbers,
.line-numbers a {
@@ -13,8 +17,8 @@
}
// highlight line via anchor
- pre.hll {
- background-color: #073642 !important;
+ pre .hll {
+ background-color: #174652 !important;
}
/* Solarized Dark
diff --git a/app/assets/stylesheets/highlight/solarized_light.scss b/app/assets/stylesheets/highlight/solarized_light.scss
index 7254f4d7ac1..6b44c00c305 100644
--- a/app/assets/stylesheets/highlight/solarized_light.scss
+++ b/app/assets/stylesheets/highlight/solarized_light.scss
@@ -1,6 +1,10 @@
/* https://gist.github.com/qguv/7936275 */
+pre.code.highlight.solarized-light,
.code.solarized-light {
+ background-color: #fdf6e3;
+ color: #586e75;
+
pre.code,
.line-numbers,
.line-numbers a {
@@ -13,8 +17,8 @@
}
// highlight line via anchor
- pre.hll {
- background-color: #eee8d5 !important;
+ pre .hll {
+ background-color: #ddd8c5 !important;
}
/* Solarized Light
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 4d6f5dfd91e..a52ffc971d1 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,6 +1,10 @@
/* https://github.com/aahan/pygments-github-style */
+pre.code.highlight.white,
.code.white {
+ background-color: #fff;
+ color: #333;
+
pre.highlight,
.line-numbers,
.line-numbers a {
@@ -13,7 +17,7 @@
}
// highlight line via anchor
- pre.hll {
+ pre .hll {
background-color: #f8eec7 !important;
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index f46d6542c03..e7125c03993 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -30,7 +30,8 @@
color: #666;
font-size: 14px;
font-weight: normal;
- padding: 10px 0;
+ padding: 3px 0;
+ margin-bottom: 10px;
}
.commit-info-row {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index e167d044e47..84361e15481 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -1,17 +1,11 @@
.commits-compare-switch{
+ @extend .btn;
background: image-url("switch_icon.png") no-repeat center center;
- width: 32px;
- height: 32px;
text-indent: -9999px;
float: left;
margin-right: 9px;
- border: 1px solid #DDD;
- @include border-radius(4px);
- padding: 4px;
- background-color: #EEE;
}
-
.lists-separator {
margin: 10px 0;
border-color: #DDD;
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 96f84b7122b..af9c83e5dc8 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -23,30 +23,15 @@
}
}
-.dash-sidebar-tabs {
- margin-bottom: 2px;
- border: none;
- margin: 0 !important;
-
- li {
- &.active {
- a {
- background-color: whitesmoke !important;
- border-bottom: 1px solid whitesmoke !important;
- }
- }
-
- a {
- border-color: #DDD !important;
- }
- }
-}
-
.project-row, .group-row {
padding: 0 !important;
font-size: 14px;
line-height: 24px;
+ .str-truncated {
+ max-width: 72%;
+ }
+
a {
display: block;
padding: 8px 15px;
@@ -106,25 +91,3 @@
margin-right: 5px;
width: 16px;
}
-
-.dash-new-project {
- background: $gl-success;
- border: 1px solid $gl-success;
-
- a {
- color: #FFF;
- }
-}
-
-.dash-new-group {
- background: $gl-success;
- border: 1px solid $gl-success;
-
- a {
- color: #FFF;
- }
-}
-
-.dash-list .str-truncated {
- max-width: 72%;
-}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 54311a68852..af6ea58382f 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,25 +1,37 @@
.diff-file {
- border: 1px solid #CCC;
+ border: 1px solid $border-color;
margin-bottom: 1em;
.diff-header {
- @extend .clearfix;
- background: #EEE;
- border-bottom: 1px solid #CCC;
- padding: 5px 5px 5px 10px;
+ position: relative;
+ background: $background-color;
+ border-bottom: 1px solid $border-color;
+ padding: 10px 15px;
color: #555;
z-index: 10;
> span {
font-family: $monospace_font;
- line-height: 2;
+ word-break: break-all;
+ margin-right: 200px;
+ display: block;
+
+ .file-mode {
+ margin-left: 10px;
+ color: #777;
+ }
}
.diff-btn-group {
float: right;
+ position: absolute;
+ top: 5px;
+ right: 15px;
.btn {
- background-color: #FFF;
+ padding: 0px 10px;
+ font-size: 13px;
+ line-height: 28px;
}
}
@@ -27,11 +39,6 @@
font-family: $monospace_font;
font-size: smaller;
}
-
- .file-mode {
- font-family: $monospace_font;
- margin-left: 10px;
- }
}
.diff-content {
overflow: auto;
@@ -84,10 +91,10 @@
margin: 0px;
padding: 0px;
border: none;
- background: #F5F5F5;
+ background: $background-color;
color: rgba(0,0,0,0.3);
padding: 0px 5px;
- border-right: 1px solid #ccc;
+ border-right: 1px solid $border-color;
text-align: right;
min-width: 35px;
max-width: 50px;
@@ -136,7 +143,7 @@
background: #ffecec;
}
&.matched {
- color: #ccc;
+ color: $border-color;
background: #fafafa;
}
&.parallel {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 88aa256e56e..759ba6b1c22 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -16,8 +16,6 @@
}
}
.commit-button-annotation {
- @extend .alert;
- @extend .alert-info;
display: inline-block;
margin: 0;
padding: 2px;
@@ -39,7 +37,7 @@
}
.editor-ref {
- background: #f5f5f5;
+ background: $background-color;
padding: 11px 15px;
border-right: 1px solid #CCC;
display: inline-block;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 3e9e36e477e..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 {
@@ -154,10 +155,12 @@
overflow: auto;
.event-last-push-text {
@include str-truncated(100%);
+ padding: 5px 0;
+ font-size: 13px;
float:left;
margin-right: -150px;
padding-right: 150px;
- line-height: 24px;
+ line-height: 20px;
}
}
@@ -188,7 +191,7 @@
li a {
font-size: 13px;
padding: 5px 10px;
- background: rgba(0,0,0,0.045);
+ background: $background-color;
margin-left: 4px;
}
}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 3d878d1e528..c3b10d144e1 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -1,11 +1,11 @@
.project-network {
- border: 1px solid #CCC;
+ border: 1px solid $border-color;
.controls {
color: #888;
font-size: 14px;
padding: 5px;
- border-bottom: 1px solid #bbb;
+ border-bottom: 1px solid $border-color;
background: #EEE;
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index e49fe1a9dd6..2b1b747139a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -1,6 +1,5 @@
.new-group-member-holder {
margin-top: 50px;
- background: #f9f9f9;
padding-top: 20px;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d8d12338859..586e7b5f8da 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -19,13 +19,13 @@
&.affix {
position: fixed;
top: 70px;
- width: 220px;
+ margin-right: 35px;
}
}
}
.issuable-context-title {
- font-size: 15px;
+ font-size: 14px;
line-height: 1.4;
margin-bottom: 5px;
@@ -39,3 +39,9 @@
margin-right: 4px;
}
}
+
+.issuable-affix .context {
+ font-size: 13px;
+
+ .btn { font-size: 13px; }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 4ea34cc1dac..3572f33e91f 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -25,28 +25,16 @@
display: inline-block;
}
- .issue-actions {
- display: none;
- position: absolute;
- top: 10px;
- right: 15px;
- }
-
- &:hover {
- .issue-actions {
- display: block;
- }
+ .issue-no-comments {
+ opacity: 0.5;
}
}
}
.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,33 +47,10 @@
}
}
-@media (min-width: 800px) { .issues_filters select { width: 160px; } }
-@media (min-width: 1200px) { .issues_filters select { width: 220px; } }
-
-@media (min-width: 800px) { .issues_bulk_update .select2-container { min-width: 120px; } }
-@media (min-width: 1200px) { .issues_bulk_update .select2-container { min-width: 160px; } }
-
-.issues_bulk_update {
- .select2-container .select2-choice {
- color: #444 !important;
- font-weight: 500;
- }
-}
-
-#update_status {
- width: 100px;
-}
-
.participants {
margin-bottom: 20px;
}
-.issues_bulk_update {
- .select2-container {
- text-shadow: none;
- }
-}
-
.issue-search-form {
margin: 0;
height: 24px;
@@ -119,12 +84,12 @@ form.edit-issue {
}
&.closed {
- background: #F5f5f5;
+ background: #F9F9F9;
border-color: #E5E5E5;
}
&.merged {
- background: #F5f5f5;
+ background: #F9F9F9;
border-color: #E5E5E5;
}
}
@@ -177,6 +142,6 @@ h2.issue-title {
font-weight: bold;
}
-.context .select2-container {
- width: 100% !important;
+.issue-form .select2-container {
+ width: 250px !important;
}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index d366300511e..83b866c3a64 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -113,3 +113,12 @@
}
}
}
+
+.oauth-image-link {
+ margin-right: 10px;
+
+ img {
+ width: 32px;
+ height: 32px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 9bd34b7376f..3165396a94d 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -12,14 +12,8 @@
}
.accept-merge-holder {
- margin-top: 5px;
-
.accept-action {
display: inline-block;
-
- .accept_merge_request {
- padding: 10px 20px;
- }
}
.accept-control {
@@ -97,11 +91,16 @@
.merge-request-info {
color: #999;
font-size: 13px;
-
- .merge-request-labels {
- display: inline-block;
- }
}
+
+ }
+
+ .merge-request-labels {
+ display: inline-block;
+ }
+
+ .merge-request-no-comments {
+ opacity: 0.5;
}
}
@@ -123,7 +122,8 @@
}
.mr-state-widget {
- background: $box_bg;
+ font-size: 13px;
+ background: #F9F9F9;
margin-bottom: 20px;
color: #666;
border: 1px solid #EEE;
@@ -134,7 +134,7 @@
font-size: 15px;
border-bottom: 1px solid #BBB;
color: #777;
- background-color: #F5F5F5;
+ background-color: $background-color;
&.ci-success {
color: $gl-success;
@@ -142,24 +142,15 @@
background-color: #F1FAF1;
}
- &.ci-pending {
- color: #548;
- border-color: #548;
- background-color: #F4F1FA;
- }
-
+ &.ci-pending,
&.ci-running {
color: $gl-warning;
border-color: $gl-warning;
background-color: #FAF5F1;
}
- &.ci-failed {
- color: $gl-danger;
- border-color: $gl-danger;
- background-color: #FAF1F1;
- }
-
+ &.ci-failed,
+ &.ci-canceled,
&.ci-error {
color: $gl-danger;
border-color: $gl-danger;
@@ -199,3 +190,7 @@
}
}
}
+
+.merge-request-form .select2-container {
+ width: 250px !important;
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 384ff6d740c..589a43c4264 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -62,21 +62,19 @@ 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";
- }
+ // Reduce left padding of first ul element
+ ul.task-list:first-child {
+ padding-left: 10px;
- &:hover:before {
- text-decoration: none;
+ // sub-lists should be padded normally
+ ul {
+ padding-left: 20px;
}
}
+
+ hr {
+ margin: 10px 0;
+ }
}
}
.note-header {
@@ -89,6 +87,16 @@ ul.notes {
}
}
+// Diff code in discussion view
+.discussion-body .diff-file {
+ .diff-header > span {
+ margin-right: 10px;
+ }
+ .line_content {
+ white-space: pre-wrap;
+ }
+}
+
.diff-file .notes_holder {
font-size: 13px;
line-height: 18px;
@@ -138,7 +146,7 @@ ul.notes {
display: none;
float: right;
- [class~="fa"] {
+ i.fa {
font-size: 16px;
line-height: 16px;
vertical-align: middle;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 0ab62b7ae49..280e8b57174 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,31 +1,7 @@
.account-page {
fieldset {
margin-bottom: 15px;
- border-bottom: 1px dashed #ddd;
padding-bottom: 15px;
-
- &:last-child {
- border: none;
- }
-
- legend {
- border: none;
- margin-bottom: 10px;
- }
- }
-}
-
-.oauth_select_holder {
- img {
- padding: 2px;
- margin-right: 10px;
- }
- .active {
- img {
- border: 1px solid #4BD;
- background: $hover;
- @include border-radius(5px);
- }
}
}
@@ -41,11 +17,6 @@
}
}
-.user-show-username {
- font-weight: 200;
- color: #666;
-}
-
/*
* Appearance settings
*
@@ -66,7 +37,7 @@
}
&.default {
- background: #f1f1f1;
+ background: #888888;
}
&.modern {
@@ -80,6 +51,10 @@
&.violet {
background: #548;
}
+
+ &.blue {
+ background: #2980b9;
+ }
}
}
}
@@ -102,3 +77,19 @@
}
}
}
+
+.oauth-buttons {
+ .btn-group {
+ margin-right: 10px;
+ }
+
+ .btn {
+ line-height: 36px;
+ height: 56px;
+
+ img {
+ width: 32px;
+ height: 32px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bfd05973d75..5a8d4665294 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -15,82 +15,76 @@
}
.project-home-panel {
- margin-bottom: 15px;
+ margin-bottom: 20px;
position: relative;
- padding-left: 85px;
-
- &.empty-project {
- border-bottom: 0px;
- padding-bottom: 15px;
- margin-bottom: 0px;
- }
+ padding-left: 65px;
+ border-bottom: 1px solid #DDD;
+ padding-bottom: 10px;
+ padding-top: 5px;
+ min-height: 50px;
.project-identicon-holder {
position: absolute;
left: 0;
+ top: -10px;
.avatar {
- width: 70px;
- height: 70px;
+ width: 50px;
+ height: 50px;
}
.identicon {
- font-size: 45px;
- line-height: 1.6;
+ font-size: 26px;
+ line-height: 50px;
}
}
- .project-home-dropdown {
- margin-left: 10px;
- float: right;
- }
-
.project-home-row {
@extend .clearfix;
margin-bottom: 15px;
+ &.project-home-row-top {
+ margin-bottom: 15px;
+ }
+
.project-home-desc {
- float: left;
- color: #666;
font-size: 16px;
+ line-height: 1.3;
+ margin-right: 250px;
}
- .star-fork-buttons {
- float: right;
- min-width: 200px;
- font-size: 14px;
- font-weight: bold;
-
- .star-buttons, .fork-buttons {
- float: right;
- margin-left: 20px;
-
- a:hover {
- text-decoration: none;
- }
-
- .count {
- margin-left: 5px;
- }
- }
+ .project-home-desc {
+ float: left;
+ color: $gray;
}
}
.visibility-level-label {
- color: #555;
- font-weight: bold;
+ color: $gray;
i {
color: inherit;
}
}
-}
-.project-home-links {
- padding: 10px 0px;
- float: right;
- a {
- margin-left: 10px;
- font-weight: 500;
+ .project-repo-buttons {
+ margin-top: -3px;
+ position: absolute;
+ right: 0;
+ width: 265px;
+ 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;
+ }
+ }
}
}
@@ -105,6 +99,19 @@
background: #FAFAFA;
width: 100%;
}
+
+ .input-group-addon {
+ background: #FAFAFA;
+
+ &.git-protocols {
+ padding: 0;
+ border: none;
+
+ .input-group-btn:last-child > .btn {
+ @include border-radius-right(0);
+ }
+ }
+ }
}
.project-visibility-level-holder {
@@ -123,7 +130,7 @@
.option-descr {
margin-left: 24px;
- color: #666;
+ color: $gray;
}
}
}
@@ -156,7 +163,7 @@ ul.nav.nav-projects-tabs {
}
}
-.team_member_row form {
+.project_member_row form {
margin: 0px;
}
@@ -195,47 +202,29 @@ ul.nav.nav-projects-tabs {
}
.project-side {
- .btn-block {
- background-image: none;
-
- .btn, &.btn {
- white-space: normal;
- text-align: left;
- padding: 10px 15px;
- background-color: #F9F9F9;
- border-color: #DDD;
+ .project-fork-icon {
+ float: left;
+ font-size: 26px;
+ margin-right: 10px;
+ line-height: 1.5;
+ }
- &:hover {
- background-color: #eee;
- border-color: #DDD;
- }
- }
+ .well {
+ padding: 14px;
- .count {
- float: right;
- font-weight: 500;
- text-shadow: 0 1px #FFF;
+ h4 {
+ font-weight: normal;
+ margin: 0;
+ color: #555;
}
- &.btn-group-justified {
- .btn {
- width: 100%;
- }
- .dropdown-toggle {
- width: 30px;
- padding: 10px;
- }
- ul {
- width: 100%;
- }
+ .nav-pills a {
+ padding: 10px;
}
- }
- .project-fork-icon {
- float: left;
- font-size: 26px;
- margin-right: 10px;
- line-height: 1.5;
+ .nav {
+ margin: 10px 0;
+ }
}
}
@@ -301,3 +290,8 @@ table.table.protected-branches-list tr.no-border {
border: 0;
}
}
+
+.project-import .btn {
+ float: left;
+ margin-right: 10px;
+}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index a4337c11ab7..57f63b52aa1 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -27,7 +27,7 @@
}
&.selected {
td {
- background: #f5f5f5;
+ background: $background-color;
border-top: 1px solid #EEE;
border-bottom: 1px solid #EEE;
}
@@ -40,8 +40,8 @@
max-width: 320px;
vertical-align: middle;
- i {
- color: $gl-info;
+ i, a {
+ color: $gl-link-color;
}
img {
@@ -61,13 +61,18 @@
.tree_author {
padding-right: 8px;
+
+ .commit-author-name {
+ color: gray;
+ }
}
.tree_commit {
color: gray;
.tree-commit-link {
- color: #444;
+ color: gray;
+
&:hover {
text-decoration: underline;
}
@@ -107,7 +112,7 @@
.tree-ref-holder {
float: left;
- margin-right: 6px;
+ margin-right: 15px;
.select2-container .select2-choice, .select2-container.select2-drop-above .select2-choice {
padding: 4px 12px;
diff --git a/app/assets/stylesheets/pages/votes.scss b/app/assets/stylesheets/pages/votes.scss
index ba0a519dca6..dc9a7d71e8b 100644
--- a/app/assets/stylesheets/pages/votes.scss
+++ b/app/assets/stylesheets/pages/votes.scss
@@ -1,39 +1,4 @@
-.votes {
- font-size: 13px;
- line-height: 15px;
- .progress {
- height: 4px;
- margin: 0;
- .bar {
- float: left;
- height: 100%;
- }
- .bar-success {
- @include linear-gradient(#62C462, #51A351);
- background-color: #468847;
- }
- .bar-danger {
- @include linear-gradient(#EE5F5B, #BD362F);
- background-color: #B94A48;
- }
- }
- .upvotes {
- display: inline-block;
- color: #468847;
- }
- .downvotes {
- display: inline-block;
- color: #B94A48;
- }
-}
-.votes-block {
- margin: 6px;
- .downvotes {
- float: right;
- }
-}
.votes-inline {
display: inline-block;
margin: 0 8px;
}
-
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 42dbf4d6ef3..1be0551ad3b 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -11,3 +11,7 @@ header, nav, nav.main-nav, nav.navbar-collapse, nav.navbar-collapse.collapse {di
.wiki h1 {font-size: 30px;}
.wiki h2 {font-size: 22px;}
.wiki h3 {font-size: 18px; font-weight: bold; }
+
+.sidebar-wrapper { display: none; }
+.nav { display: none; }
+.btn { display: none; }
diff --git a/app/assets/stylesheets/themes/dark-theme.scss b/app/assets/stylesheets/themes/dark-theme.scss
deleted file mode 100644
index b7b22a8724e..00000000000
--- a/app/assets/stylesheets/themes/dark-theme.scss
+++ /dev/null
@@ -1,63 +0,0 @@
-@mixin dark-theme($color-light, $color, $color-darker, $color-dark) {
- header {
- &.navbar-gitlab {
- .navbar-inner {
- background: $color;
-
- .navbar-toggle {
- color: #FFF;
- }
-
- .app_logo, .navbar-toggle {
- &:hover {
- background-color: $color-darker;
- }
- }
-
- .app_logo {
- background-color: $color-dark;
- }
-
- .title {
- color: #FFF;
-
- a {
- color: #FFF;
- &:hover {
- text-decoration: underline;
- }
- }
- }
-
- .search {
- .search-input {
- background-color: $color-light;
- background-color: rgba(255, 255, 255, 0.5);
- border: 1px solid $color-light;
-
- &:focus {
- background-color: white;
- }
- }
- }
-
- .search-input::-webkit-input-placeholder {
- color: #666;
- }
-
- .nav > li > a {
- color: $color-light;
-
- &:hover, &:focus, &:active {
- background: none;
- color: #FFF;
- }
- }
-
- .search-input {
- border-color: $color-light;
- }
- }
- }
- }
-}
diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss
new file mode 100644
index 00000000000..139b3cc1ac4
--- /dev/null
+++ b/app/assets/stylesheets/themes/gitlab-theme.scss
@@ -0,0 +1,70 @@
+@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
+ header {
+ &.navbar-gitlab {
+ .app_logo {
+ background-color: $color-darker;
+
+ a {
+ color: $color-light;
+ }
+
+ &:hover {
+ background-color: $color-dark;
+ a {
+ color: #FFF;
+ }
+ }
+ }
+ }
+ }
+
+ .page-with-sidebar {
+ background: $color-darker;
+
+ .collapse-nav a {
+ color: #FFF;
+ background: $color;
+ }
+
+ .sidebar-wrapper {
+ background: $color-darker;
+ border-right: 1px solid $color-darker;
+ }
+
+ .nav-sidebar li {
+ a {
+ color: $color-light;
+
+ &:hover, &:focus, &:active {
+ background: $color-dark;
+ }
+
+ i {
+ color: $color-light;
+ }
+
+ .count {
+ color: $color-light;
+ background: $color-dark;
+ }
+ }
+
+ &.separate-item {
+ border-top: 1px solid $color;
+ }
+
+ &.active a {
+ color: #FFF;
+ font-weight: bold;
+
+ &.no-highlight {
+ border: none;
+ }
+
+ i {
+ color: #FFF
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/themes/ui_basic.scss b/app/assets/stylesheets/themes/ui_basic.scss
index 097d5c5b73c..63e8dce1e92 100644
--- a/app/assets/stylesheets/themes/ui_basic.scss
+++ b/app/assets/stylesheets/themes/ui_basic.scss
@@ -4,27 +4,5 @@
*
*/
.ui_basic {
- header {
- &.navbar-gitlab {
- .navbar-inner {
- background: #F1F1F1;
- border-bottom: 1px solid #DDD;
-
- .title {
- color: #555;
-
- a {
- color: #555;
- &:hover {
- text-decoration: underline;
- }
- }
- }
-
- .nav > li > a {
- color: $style_color;
- }
- }
- }
- }
+ @include gitlab-theme(#CCCCCC, #888888, #777777, #666666);
}
diff --git a/app/assets/stylesheets/themes/ui_blue.scss b/app/assets/stylesheets/themes/ui_blue.scss
new file mode 100644
index 00000000000..cf995622b6b
--- /dev/null
+++ b/app/assets/stylesheets/themes/ui_blue.scss
@@ -0,0 +1,6 @@
+/**
+ * Blue GitLab UI theme
+ */
+.ui_blue {
+ @include gitlab-theme(#BECDE9, #2980b9, #1970a9, #096099);
+}
diff --git a/app/assets/stylesheets/themes/ui_color.scss b/app/assets/stylesheets/themes/ui_color.scss
index 7ac6903b2e4..6babccec0da 100644
--- a/app/assets/stylesheets/themes/ui_color.scss
+++ b/app/assets/stylesheets/themes/ui_color.scss
@@ -2,5 +2,5 @@
* Violet GitLab UI theme
*/
.ui_color {
- @include dark-theme(#98C, #548, #436, #325);
+ @include gitlab-theme(#98C, #548, #436, #325);
}
diff --git a/app/assets/stylesheets/themes/ui_gray.scss b/app/assets/stylesheets/themes/ui_gray.scss
index 9257e5f4d40..f8e4a6ea7da 100644
--- a/app/assets/stylesheets/themes/ui_gray.scss
+++ b/app/assets/stylesheets/themes/ui_gray.scss
@@ -2,5 +2,5 @@
* Gray GitLab UI theme
*/
.ui_gray {
- @include dark-theme(#979797, #373737, #272727, #222222);
+ @include gitlab-theme(#979797, #373737, #272727, #222222);
}
diff --git a/app/assets/stylesheets/themes/ui_mars.scss b/app/assets/stylesheets/themes/ui_mars.scss
index 4caf5843d9b..fda96b64cd9 100644
--- a/app/assets/stylesheets/themes/ui_mars.scss
+++ b/app/assets/stylesheets/themes/ui_mars.scss
@@ -2,5 +2,5 @@
* Classic GitLab UI theme
*/
.ui_mars {
- @include dark-theme(#979DA7, #474D57, #373D47, #24272D);
+ @include gitlab-theme(#979DA7, #474D57, #373D47, #24272D);
}
diff --git a/app/assets/stylesheets/themes/ui_modern.scss b/app/assets/stylesheets/themes/ui_modern.scss
index 70449882317..8261e80b35f 100644
--- a/app/assets/stylesheets/themes/ui_modern.scss
+++ b/app/assets/stylesheets/themes/ui_modern.scss
@@ -2,5 +2,5 @@
* Modern GitLab UI theme
*/
.ui_modern {
- @include dark-theme(#ADC, #019875, #018865, #017855);
+ @include gitlab-theme(#ADC, #019875, #018865, #017855);
}
diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb
index 6a8f20f6047..56e24386463 100644
--- a/app/controllers/admin/application_controller.rb
+++ b/app/controllers/admin/application_controller.rb
@@ -2,8 +2,8 @@
#
# Automatically sets the layout and ensures an administrator is logged in
class Admin::ApplicationController < ApplicationController
+ before_action :authenticate_admin!
layout 'admin'
- before_filter :authenticate_admin!
def authenticate_admin!
return render_404 unless current_user.is_admin?
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 5973af71267..03fd12e9ecd 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -1,5 +1,5 @@
class Admin::ApplicationSettingsController < Admin::ApplicationController
- before_filter :set_application_setting
+ before_action :set_application_setting
def show
end
@@ -20,6 +20,15 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
end
def application_setting_params
+ restricted_levels = params[:application_setting][:restricted_visibility_levels]
+ if restricted_levels.nil?
+ params[:application_setting][:restricted_visibility_levels] = []
+ else
+ restricted_levels.map! do |level|
+ level.to_i
+ end
+ end
+
params.require(:application_setting).permit(
:default_projects_limit,
:default_branch_protection,
@@ -29,6 +38,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:twitter_sharing_enabled,
:sign_in_text,
:home_page_url,
+ :max_attachment_size,
+ :default_project_visibility,
+ :default_snippet_visibility,
+ :restricted_signup_domains_raw,
+ restricted_visibility_levels: [],
:version_check_enabled
)
end
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index e1643bb34bf..0808024fc39 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,5 +1,5 @@
class Admin::BroadcastMessagesController < Admin::ApplicationController
- before_filter :broadcast_messages
+ before_action :broadcast_messages
def index
@broadcast_message = BroadcastMessage.new
diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb
new file mode 100644
index 00000000000..c301e61d1c7
--- /dev/null
+++ b/app/controllers/admin/deploy_keys_controller.rb
@@ -0,0 +1,49 @@
+class Admin::DeployKeysController < Admin::ApplicationController
+ before_action :deploy_keys, only: [:index]
+ before_action :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 65dc027c8eb..2dfae13ac5c 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -1,16 +1,16 @@
class Admin::GroupsController < Admin::ApplicationController
- before_filter :group, only: [:edit, :show, :update, :destroy, :project_update, :project_teams_update]
+ before_action :group, only: [:edit, :show, :update, :destroy, :project_update, :members_update]
def index
@groups = Group.all
@groups = @groups.sort(@sort = params[:sort])
@groups = @groups.search(params[:name]) if params[:name].present?
- @groups = @groups.page(params[:page]).per(20)
+ @groups = @groups.page(params[:page]).per(PER_PAGE)
end
def show
- @members = @group.members.order("access_level DESC").page(params[:members_page]).per(30)
- @projects = @group.projects.page(params[:projects_page]).per(30)
+ @members = @group.members.order("access_level DESC").page(params[:members_page]).per(PER_PAGE)
+ @projects = @group.projects.page(params[:projects_page]).per(PER_PAGE)
end
def new
@@ -40,8 +40,8 @@ class Admin::GroupsController < Admin::ApplicationController
end
end
- def project_teams_update
- @group.add_users(params[:user_ids].split(','), params[:access_level])
+ def members_update
+ @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/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index 21111bb44f5..cb33fdd9763 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -1,5 +1,5 @@
class Admin::KeysController < Admin::ApplicationController
- before_filter :user, only: [:show, :destroy]
+ before_action :user, only: [:show, :destroy]
def show
@key = user.keys.find(params[:id])
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 2b1fc862b7f..ee449badf59 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -1,7 +1,7 @@
class Admin::ProjectsController < Admin::ApplicationController
- before_filter :project, only: [:show, :transfer]
- before_filter :group, only: [:show, :transfer]
- before_filter :repository, only: [:show, :transfer]
+ before_action :project, only: [:show, :transfer]
+ before_action :group, only: [:show, :transfer]
+ before_action :repository, only: [:show, :transfer]
def index
@projects = Project.all
@@ -11,15 +11,15 @@ class Admin::ProjectsController < Admin::ApplicationController
@projects = @projects.abandoned if params[:abandoned].present?
@projects = @projects.search(params[:name]) if params[:name].present?
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(20)
+ @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page]).per(PER_PAGE)
end
def show
if @group
- @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(30)
+ @group_members = @group.members.order("access_level DESC").page(params[:group_members_page]).per(PER_PAGE)
end
- @project_members = @project.project_members.page(params[:project_members_page]).per(30)
+ @project_members = @project.project_members.page(params[:project_members_page]).per(PER_PAGE)
end
def transfer
diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb
index 44a3f1379d8..a62170662e1 100644
--- a/app/controllers/admin/services_controller.rb
+++ b/app/controllers/admin/services_controller.rb
@@ -1,5 +1,5 @@
class Admin::ServicesController < Admin::ApplicationController
- before_filter :service, only: [:edit, :update]
+ before_action :service, only: [:edit, :update]
def index
@services = services_templates
@@ -40,13 +40,6 @@ class Admin::ServicesController < Admin::ApplicationController
def application_services_params
params.permit(:id,
- service: [
- :title, :token, :type, :active, :api_key, :subdomain,
- :room, :recipients, :project_url, :webhook,
- :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch,
- :send_from_committer_email, :disable_diffs
- ])
+ service: Projects::ServicesController::ALLOWED_PARAMS)
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 693970e5349..d36e359934c 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -1,5 +1,5 @@
class Admin::UsersController < Admin::ApplicationController
- before_filter :user, only: [:show, :edit, :update, :destroy]
+ before_action :user, only: [:show, :edit, :update, :destroy]
def index
@users = User.order_name_asc.filter(params[:filter])
@@ -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
@@ -102,8 +102,7 @@ class Admin::UsersController < Admin::ApplicationController
email = user.emails.find(params[:email_id])
email.destroy
- user.set_notification_email
- user.save if user.notification_email_changed?
+ user.update_secondary_emails!
respond_to do |format|
format.html { redirect_to :back, notice: "Successfully removed email." }
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index df1a588313e..eee10d6c22a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -3,16 +3,19 @@ require 'gon'
class ApplicationController < ActionController::Base
include Gitlab::CurrentSettings
include GitlabRoutingHelper
+ include PageLayoutHelper
- before_filter :authenticate_user_from_token!
- before_filter :authenticate_user!
- before_filter :reject_blocked!
- before_filter :check_password_expiration
- before_filter :ldap_security_check
- before_filter :default_headers
- before_filter :add_gon_variables
- before_filter :configure_permitted_parameters, if: :devise_controller?
- before_filter :require_email, unless: :devise_controller?
+ PER_PAGE = 20
+
+ before_action :authenticate_user_from_token!
+ before_action :authenticate_user!
+ before_action :reject_blocked!
+ before_action :check_password_expiration
+ before_action :ldap_security_check
+ before_action :default_headers
+ before_action :add_gon_variables
+ before_action :configure_permitted_parameters, if: :devise_controller?
+ before_action :require_email, unless: :devise_controller?
protect_from_forgery with: :exception
@@ -85,6 +88,10 @@ class ApplicationController < ActionController::Base
end
end
+ def after_sign_out_path_for(resource)
+ new_user_session_path
+ end
+
def abilities
Ability.abilities
end
@@ -124,7 +131,7 @@ class ApplicationController < ActionController::Base
def repository
@repository ||= project.repository
- rescue Grit::NoSuchPathError(e)
+ rescue Grit::NoSuchPathError => e
log_exception(e)
nil
end
@@ -151,7 +158,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
@@ -189,6 +196,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
@@ -279,40 +287,15 @@ class ApplicationController < ActionController::Base
@filter_params
end
- def set_filter_values(collection)
- assignee_id = @filter_params[:assignee_id]
- author_id = @filter_params[:author_id]
- milestone_id = @filter_params[:milestone_id]
-
- @sort = @filter_params[:sort]
- @assignees = User.where(id: collection.pluck(:assignee_id))
- @authors = User.where(id: collection.pluck(:author_id))
- @milestones = Milestone.where(id: collection.pluck(:milestone_id))
-
- if assignee_id.present? && !assignee_id.to_i.zero?
- @assignee = @assignees.find_by(id: assignee_id)
- end
-
- if author_id.present? && !author_id.to_i.zero?
- @author = @authors.find_by(id: author_id)
- end
-
- if milestone_id.present? && !milestone_id.to_i.zero?
- @milestone = @milestones.find_by(id: milestone_id)
- end
- end
-
def get_issues_collection
set_filters_params
issues = IssuesFinder.new.execute(current_user, @filter_params)
- set_filter_values(issues)
issues
end
def get_merge_requests_collection
set_filters_params
merge_requests = MergeRequestsFinder.new.execute(current_user, @filter_params)
- set_filter_values(merge_requests)
merge_requests
end
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/dashboard/application_controller.rb b/app/controllers/dashboard/application_controller.rb
new file mode 100644
index 00000000000..962ea38d6c9
--- /dev/null
+++ b/app/controllers/dashboard/application_controller.rb
@@ -0,0 +1,3 @@
+class Dashboard::ApplicationController < ApplicationController
+ layout 'dashboard'
+end
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 61d691e6368..3bc94ff2187 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,21 +1,5 @@
-class Dashboard::GroupsController < ApplicationController
+class Dashboard::GroupsController < Dashboard::ApplicationController
def index
- @user_groups = current_user.group_members.page(params[:page]).per(20)
- end
-
- def leave
- @users_group = group.group_members.where(user_id: current_user.id).first
- if can?(current_user, :destroy, @users_group)
- @users_group.destroy
- redirect_to(dashboard_groups_path, info: "You left #{group.name} group.")
- else
- return render_403
- end
- end
-
- private
-
- def group
- @group ||= Group.find_by(path: params[:id])
+ @group_members = current_user.group_members.page(params[:page]).per(PER_PAGE)
end
end
diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb
index 386e283f3a0..53896d4f2c7 100644
--- a/app/controllers/dashboard/milestones_controller.rb
+++ b/app/controllers/dashboard/milestones_controller.rb
@@ -1,5 +1,5 @@
-class Dashboard::MilestonesController < ApplicationController
- before_filter :load_projects
+class Dashboard::MilestonesController < Dashboard::ApplicationController
+ before_action :load_projects
def index
project_milestones = case params[:state]
@@ -8,7 +8,7 @@ class Dashboard::MilestonesController < ApplicationController
else state('active')
end
@dashboard_milestones = Milestones::GroupService.new(project_milestones).execute
- @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(30)
+ @dashboard_milestones = Kaminari.paginate_array(@dashboard_milestones).page(params[:page]).per(PER_PAGE)
end
def show
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 56e6fcc41ca..da96171e885 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,5 +1,5 @@
-class Dashboard::ProjectsController < ApplicationController
- before_filter :event_filter
+class Dashboard::ProjectsController < Dashboard::ApplicationController
+ before_action :event_filter
def starred
@projects = current_user.starred_projects
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index 8f06a673584..17ddde68f93 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,19 +1,13 @@
-class DashboardController < ApplicationController
+class DashboardController < Dashboard::ApplicationController
+ before_action :load_projects, except: [:projects]
+ before_action :event_filter, only: :show
+
respond_to :html
- before_filter :load_projects, except: [:projects]
- before_filter :event_filter, only: :show
-
def show
- @projects_limit = 20
- @groups = current_user.authorized_groups.order_name_asc
- @has_authorized_projects = @projects.count > 0
- @projects_count = @projects.count
@projects = @projects.includes(:namespace)
@last_push = current_user.recent_push
- @publicish_project_count = Project.publicish(current_user).count
-
respond_to do |format|
format.html
@@ -29,38 +23,15 @@ class DashboardController < ApplicationController
end
end
- def projects
- @projects = case params[:scope]
- when 'personal' then
- current_user.namespace.projects
- when 'joined' then
- current_user.authorized_projects.joined(current_user)
- when 'owned' then
- current_user.owned_projects
- else
- current_user.authorized_projects
- end
-
- @projects = @projects.where(namespace_id: Group.find_by(name: params[:group])) if params[:group].present?
- @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
- @projects = @projects.includes(:namespace, :forked_from_project, :tags)
- @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.page(params[:page]).per(30)
-
- @tags = current_user.authorized_projects.tags_on(:tags)
- @groups = current_user.authorized_groups
- end
-
def merge_requests
@merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(20)
+ @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
end
def issues
@issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(20)
+ @issues = @issues.page(params[:page]).per(PER_PAGE)
@issues = @issues.preload(:author, :project)
respond_to do |format|
diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb
new file mode 100644
index 00000000000..4b275033d26
--- /dev/null
+++ b/app/controllers/explore/application_controller.rb
@@ -0,0 +1,3 @@
+class Explore::ApplicationController < ApplicationController
+ layout 'explore'
+end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index ada7031fea4..55cda0cff17 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,13 +1,11 @@
-class Explore::GroupsController < ApplicationController
- skip_before_filter :authenticate_user!,
+class Explore::GroupsController < Explore::ApplicationController
+ skip_before_action :authenticate_user!,
:reject_blocked, :set_current_user_for_observers
- layout "explore"
-
def index
@groups = GroupsFinder.new.execute(current_user)
@groups = @groups.search(params[:search]) if params[:search].present?
@groups = @groups.sort(@sort = params[:sort])
- @groups = @groups.page(params[:page]).per(20)
+ @groups = @groups.page(params[:page]).per(PER_PAGE)
end
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 0e5891ae807..e9bcb44f6b3 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,24 +1,25 @@
-class Explore::ProjectsController < ApplicationController
- skip_before_filter :authenticate_user!,
+class Explore::ProjectsController < Explore::ApplicationController
+ skip_before_action :authenticate_user!,
:reject_blocked
- layout 'explore'
-
def index
@projects = ProjectsFinder.new.execute(current_user)
+ @tags = @projects.tags_on(:tags)
+ @projects = @projects.tagged_with(params[:tag]) if params[:tag].present?
+ @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
@projects = @projects.search(params[:search]) if params[:search].present?
@projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).page(params[:page]).per(20)
+ @projects = @projects.includes(:namespace).page(params[:page]).per(PER_PAGE)
end
def trending
@trending_projects = TrendingProjectsFinder.new.execute(current_user)
- @trending_projects = @trending_projects.page(params[:page]).per(10)
+ @trending_projects = @trending_projects.page(params[:page]).per(PER_PAGE)
end
def starred
@starred_projects = ProjectsFinder.new.execute(current_user)
@starred_projects = @starred_projects.reorder('star_count DESC')
- @starred_projects = @starred_projects.page(params[:page]).per(10)
+ @starred_projects = @starred_projects.page(params[:page]).per(PER_PAGE)
end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
new file mode 100644
index 00000000000..4df9d1b7533
--- /dev/null
+++ b/app/controllers/groups/application_controller.rb
@@ -0,0 +1,21 @@
+class Groups::ApplicationController < ApplicationController
+ layout 'group'
+
+ private
+
+ def authorize_read_group!
+ unless @group and can?(current_user, :read_group, @group)
+ if current_user.nil?
+ return authenticate_user!
+ else
+ return render_404
+ end
+ end
+ end
+
+ def authorize_admin_group!
+ unless can?(current_user, :admin_group, group)
+ return render_404
+ end
+ end
+end
diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb
index 38071410f40..6aa64222f77 100644
--- a/app/controllers/groups/avatars_controller.rb
+++ b/app/controllers/groups/avatars_controller.rb
@@ -1,6 +1,4 @@
class Groups::AvatarsController < ApplicationController
- layout "profile"
-
def destroy
@group = Group.find_by(path: params[:group_id])
@group.remove_avatar!
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index ca88d033878..a11c554a2af 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,15 +1,29 @@
-class Groups::GroupMembersController < ApplicationController
- before_filter :group
+class Groups::GroupMembersController < Groups::ApplicationController
+ skip_before_action :authenticate_user!, only: [:index]
+ before_action :group
# Authorize
- before_filter :authorize_admin_group!
+ before_action :authorize_read_group!
+ before_action :authorize_admin_group!, except: [:index, :leave]
- layout 'group'
+ 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
+ @members = @members.where(user_id: users)
+ end
+
+ @members = @members.order('access_level DESC').page(params[:page]).per(50)
+ @group_member = GroupMember.new
+ 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 members_group_path(@group), notice: 'Users were successfully added.'
+ redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
def update
@@ -18,12 +32,12 @@ class Groups::GroupMembersController < ApplicationController
end
def destroy
- @users_group = @group.group_members.find(params[:id])
+ @group_member = @group.group_members.find(params[:id])
- if can?(current_user, :destroy, @users_group) # May fail if last owner.
- @users_group.destroy
+ 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 members_group_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
@@ -31,18 +45,37 @@ class Groups::GroupMembersController < ApplicationController
end
end
- protected
+ def resend_invite
+ redirect_path = group_group_members_path(@group)
- def group
- @group ||= Group.find_by(path: params[:group_id])
+ @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 authorize_admin_group!
- unless can?(current_user, :manage_group, group)
- return render_404
+ 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, notice: "You left #{group.name} group.")
+ else
+ return render_403
end
end
+ protected
+
+ def group
+ @group ||= Group.find_by(path: params[:group_id])
+ end
+
def member_params
params.require(:group_member).permit(:access_level, :user_id)
end
diff --git a/app/controllers/groups/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb
index 6802e529b54..669f7f3126d 100644
--- a/app/controllers/groups/milestones_controller.rb
+++ b/app/controllers/groups/milestones_controller.rb
@@ -1,7 +1,5 @@
-class Groups::MilestonesController < ApplicationController
- layout 'group'
-
- before_filter :authorize_group_milestone!, only: :update
+class Groups::MilestonesController < Groups::ApplicationController
+ before_action :authorize_group_milestone!, only: :update
def index
project_milestones = case params[:state]
@@ -10,7 +8,7 @@ class Groups::MilestonesController < ApplicationController
else state('active')
end
@group_milestones = Milestones::GroupService.new(project_milestones).execute
- @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(30)
+ @group_milestones = Kaminari.paginate_array(@group_milestones).page(params[:page]).per(PER_PAGE)
end
def show
@@ -51,6 +49,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/groups_controller.rb b/app/controllers/groups_controller.rb
index d011523c94f..34f0b257db3 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -1,17 +1,16 @@
-class GroupsController < ApplicationController
- skip_before_filter :authenticate_user!, only: [:show, :issues, :members, :merge_requests]
+class GroupsController < Groups::ApplicationController
+ skip_before_action :authenticate_user!, only: [:show, :issues, :merge_requests]
respond_to :html
- before_filter :group, except: [:new, :create]
+ before_action :group, except: [:new, :create]
# Authorize
- before_filter :authorize_read_group!, except: [:new, :create]
- before_filter :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
- before_filter :authorize_create_group!, only: [:new, :create]
+ before_action :authorize_read_group!, except: [:new, :create]
+ before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
+ before_action :authorize_create_group!, only: [:new, :create]
# Load group projects
- before_filter :load_projects, except: [:new, :create, :projects, :edit, :update]
- before_filter :event_filter, only: :show
- before_filter :set_title, only: [:new, :create]
+ before_action :load_projects, except: [:new, :create, :projects, :edit, :update]
+ before_action :event_filter, only: :show
layout :determine_layout
@@ -52,13 +51,13 @@ class GroupsController < ApplicationController
def merge_requests
@merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(20)
+ @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@merge_requests = @merge_requests.preload(:author, :target_project)
end
def issues
@issues = get_issues_collection
- @issues = @issues.page(params[:page]).per(20)
+ @issues = @issues.page(params[:page]).per(PER_PAGE)
@issues = @issues.preload(:author, :project)
respond_to do |format|
@@ -67,19 +66,6 @@ class GroupsController < ApplicationController
end
end
- def members
- @project = group.projects.find(params[:project_id]) if params[:project_id]
- @members = group.group_members
-
- if params[:search].present?
- users = group.users.search(params[:search]).to_a
- @members = @members.where(user_id: users)
- end
-
- @members = @members.order('access_level DESC').page(params[:page]).per(50)
- @users_group = GroupMember.new
- end
-
def edit
end
@@ -132,23 +118,11 @@ class GroupsController < ApplicationController
end
end
- def authorize_admin_group!
- unless can?(current_user, :manage_group, group)
- return render_404
- end
- end
-
- def set_title
- @title = 'New Group'
- end
-
def determine_layout
if [:new, :create].include?(action_name.to_sym)
- 'navless'
- elsif current_user
- 'group'
+ 'application'
else
- 'public_group'
+ 'group'
end
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index c4d620d87b1..8a45dc8860d 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,15 +1,40 @@
class HelpController < ApplicationController
+ layout 'help'
+
def index
end
def show
- @category = params[:category]
- @file = params[:file]
+ @category = clean_path_info(path_params[:category])
+ @file = path_params[:file]
+
+ 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.exists?(Rails.root.join('doc', @category, @file + '.md'))
- render 'show'
- else
- not_found!
+ 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 +43,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 7dc0cac8d4c..93a7ace3530 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -3,19 +3,17 @@ class Import::BaseController < ApplicationController
private
def get_or_create_namespace
- existing_namespace = Namespace.find_by_path_or_name(@target_namespace)
-
- if existing_namespace
- if existing_namespace.owner == current_user
- namespace = existing_namespace
- else
+ begin
+ namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user)
+ namespace.add_owner(current_user)
+ rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
+ namespace = Namespace.find_by_path_or_name(@target_namespace)
+ unless current_user.can?(:create_projects, namespace)
@already_been_taken = true
return false
end
- else
- namespace = Group.create(name: @target_namespace, path: @target_namespace, owner: current_user)
- namespace.add_owner(current_user)
- namespace
end
+
+ namespace
end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 83ebc5fddca..ca78a4aaa8e 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -1,15 +1,15 @@
class Import::BitbucketController < Import::BaseController
- before_filter :verify_bitbucket_import_enabled
- before_filter :bitbucket_auth, except: :callback
+ before_action :verify_bitbucket_import_enabled
+ before_action :bitbucket_auth, except: :callback
rescue_from OAuth::Error, with: :bitbucket_unauthorized
def callback
- request_token = session.delete(:oauth_request_token)
+ request_token = session.delete(:oauth_request_token)
raise "Session expired!" if request_token.nil?
request_token.symbolize_keys!
-
+
access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
current_user.bitbucket_access_token = access_token.token
@@ -21,7 +21,7 @@ class Import::BitbucketController < Import::BaseController
def status
@repos = client.projects
-
+
@already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
already_added_projects_names = @already_added_projects.pluck(:import_source)
@@ -36,9 +36,12 @@ 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)
unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user).execute
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index dc7668ee6fd..b9f99c1b88a 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -1,6 +1,6 @@
class Import::GithubController < Import::BaseController
- before_filter :verify_github_import_enabled
- before_filter :github_auth, except: :callback
+ before_action :verify_github_import_enabled
+ before_action :github_auth, except: :callback
rescue_from Octokit::Unauthorized, with: :github_unauthorized
@@ -14,7 +14,7 @@ class Import::GithubController < Import::BaseController
def status
@repos = client.repos
client.orgs.each do |org|
- @repos += client.repos(org.login)
+ @repos += client.org_repos(org.login)
end
@already_added_projects = current_user.created_projects.where(import_type: "github")
@@ -31,9 +31,12 @@ 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)
@project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user).execute
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index e979dad4b11..1b8962d8924 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -1,6 +1,6 @@
class Import::GitlabController < Import::BaseController
- before_filter :verify_gitlab_import_enabled
- before_filter :gitlab_auth, except: :callback
+ before_action :verify_gitlab_import_enabled
+ before_action :gitlab_auth, except: :callback
rescue_from OAuth2::Error, with: :gitlab_unauthorized
@@ -13,7 +13,7 @@ class Import::GitlabController < Import::BaseController
def status
@repos = client.projects
-
+
@already_added_projects = current_user.created_projects.where(import_type: "gitlab")
already_added_projects_names = @already_added_projects.pluck(:import_source)
@@ -28,9 +28,12 @@ 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)
@project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user).execute
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
index 6067a87ee04..c121d2de7cb 100644
--- a/app/controllers/import/gitorious_controller.rb
+++ b/app/controllers/import/gitorious_controller.rb
@@ -6,7 +6,7 @@ class Import::GitoriousController < Import::BaseController
def callback
session[:gitorious_repos] = params[:repos]
- redirect_to status_import_gitorious_url
+ redirect_to status_import_gitorious_path
end
def status
diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb
new file mode 100644
index 00000000000..4aa6d28c9a8
--- /dev/null
+++ b/app/controllers/import/google_code_controller.rb
@@ -0,0 +1,117 @@
+class Import::GoogleCodeController < Import::BaseController
+ before_action :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_code_path
+ end
+
+ @repos = client.repos
+ @incompatible_repos = client.incompatible_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..eb3c8233530
--- /dev/null
+++ b/app/controllers/invites_controller.rb
@@ -0,0 +1,81 @@
+class InvitesController < ApplicationController
+ before_action :member
+ skip_before_action :authenticate_user!, only: :decline
+
+ respond_to :html
+
+ 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..83eec1bf4a2 100644
--- a/app/controllers/namespaces_controller.rb
+++ b/app/controllers/namespaces_controller.rb
@@ -1,17 +1,25 @@
class NamespacesController < ApplicationController
- skip_before_filter :authenticate_user!
+ skip_before_action :authenticate_user!
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/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index efa291d9397..507b8290a2b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,6 +1,9 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
- before_filter :authenticate_user!
- layout "profile"
+ include PageLayoutHelper
+
+ before_action :authenticate_user!
+
+ layout 'profile'
def index
head :forbidden and return
@@ -10,7 +13,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
@application = Doorkeeper::Application.new(application_params)
@application.owner = current_user
-
+
if @application.save
flash[:notice] = I18n.t(:notice, scope: [:doorkeeper, :flash, :applications, :create])
redirect_to oauth_application_url(@application)
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index a57b4a60c24..24025d8c723 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -1,6 +1,7 @@
class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
- before_filter :authenticate_resource_owner!
- layout "profile"
+ before_action :authenticate_resource_owner!
+
+ layout 'profile'
def new
if pre_auth.authorizable?
diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb
index 0b27ce7da72..3ab6def511c 100644
--- a/app/controllers/oauth/authorized_applications_controller.rb
+++ b/app/controllers/oauth/authorized_applications_controller.rb
@@ -1,5 +1,7 @@
class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController
- layout "profile"
+ include PageLayoutHelper
+
+ layout 'profile'
def destroy
Doorkeeper::AccessToken.revoke_all_for(params[:id], current_resource_owner)
diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb
index fe121691a10..175afbf8425 100644
--- a/app/controllers/profiles/accounts_controller.rb
+++ b/app/controllers/profiles/accounts_controller.rb
@@ -1,7 +1,11 @@
-class Profiles::AccountsController < ApplicationController
- layout "profile"
-
+class Profiles::AccountsController < Profiles::ApplicationController
def show
@user = current_user
end
+
+ def unlink
+ provider = params[:provider]
+ current_user.identities.find_by(provider: provider).destroy
+ redirect_to profile_account_path
+ end
end
diff --git a/app/controllers/profiles/application_controller.rb b/app/controllers/profiles/application_controller.rb
new file mode 100644
index 00000000000..c8be288b9a0
--- /dev/null
+++ b/app/controllers/profiles/application_controller.rb
@@ -0,0 +1,3 @@
+class Profiles::ApplicationController < ApplicationController
+ layout 'profile'
+end
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 57f3bbf0627..f193adb46b4 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -1,6 +1,4 @@
-class Profiles::AvatarsController < ApplicationController
- layout "profile"
-
+class Profiles::AvatarsController < Profiles::ApplicationController
def destroy
@user = current_user
@user.remove_avatar!
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 4a65c978e5c..0ede9b8e21b 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -1,6 +1,4 @@
-class Profiles::EmailsController < ApplicationController
- layout "profile"
-
+class Profiles::EmailsController < Profiles::ApplicationController
def index
@primary = current_user.email
@emails = current_user.emails
@@ -9,7 +7,11 @@ class Profiles::EmailsController < ApplicationController
def create
@email = current_user.emails.new(email_params)
- flash[:alert] = @email.errors.full_messages.first unless @email.save
+ if @email.save
+ NotificationService.new.new_email(@email)
+ else
+ flash[:alert] = @email.errors.full_messages.first
+ end
redirect_to profile_emails_url
end
@@ -18,8 +20,7 @@ class Profiles::EmailsController < ApplicationController
@email = current_user.emails.find(params[:id])
@email.destroy
- current_user.set_notification_email
- current_user.save if current_user.notification_email_changed?
+ current_user.update_secondary_emails!
respond_to do |format|
format.html { redirect_to profile_emails_url }
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 4e2bd0a9b4b..f3224148fda 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -1,6 +1,5 @@
-class Profiles::KeysController < ApplicationController
- layout "profile"
- skip_before_filter :authenticate_user!, only: [:get_keys]
+class Profiles::KeysController < Profiles::ApplicationController
+ skip_before_action :authenticate_user!, only: [:get_keys]
def index
@keys = current_user.keys
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 433c19189af..22423651c17 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -1,6 +1,4 @@
-class Profiles::NotificationsController < ApplicationController
- layout 'profile'
-
+class Profiles::NotificationsController < Profiles::ApplicationController
def show
@user = current_user
@notification = current_user.notification
@@ -14,9 +12,9 @@ class Profiles::NotificationsController < ApplicationController
@saved = if type == 'global'
current_user.update_attributes(user_params)
elsif type == 'group'
- users_group = current_user.group_members.find(params[:notification_id])
- users_group.notification_level = params[:notification_level]
- users_group.save
+ group_member = current_user.group_members.find(params[:notification_id])
+ group_member.notification_level = params[:notification_level]
+ group_member.save
else
project_member = current_user.project_members.find(params[:notification_id])
project_member.notification_level = params[:notification_level]
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index 0c614969a3f..c780e0983f9 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -1,11 +1,10 @@
-class Profiles::PasswordsController < ApplicationController
- layout :determine_layout
+class Profiles::PasswordsController < Profiles::ApplicationController
+ skip_before_action :check_password_expiration, only: [:new, :create]
- skip_before_filter :check_password_expiration, only: [:new, :create]
+ before_action :set_user
+ before_action :authorize_change_password!
- before_filter :set_user
- before_filter :set_title
- before_filter :authorize_change_password!
+ layout :determine_layout
def new
end
@@ -66,13 +65,9 @@ class Profiles::PasswordsController < ApplicationController
@user = current_user
end
- def set_title
- @title = "New password"
- end
-
def determine_layout
if [:new, :create].include?(action_name.to_sym)
- 'navless'
+ 'application'
else
'profile'
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index a7863aba756..f4366c18e7b 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -1,11 +1,9 @@
-class ProfilesController < ApplicationController
+class ProfilesController < Profiles::ApplicationController
include ActionView::Helpers::SanitizeHelper
- before_filter :user
- before_filter :authorize_change_username!, only: :update_username
- skip_before_filter :require_email, only: [:show, :update]
-
- layout 'profile'
+ before_action :user
+ before_action :authorize_change_username!, only: :update_username
+ skip_before_action :require_email, only: [:show, :update]
def show
end
@@ -25,7 +23,8 @@ class ProfilesController < ApplicationController
if @user.update_attributes(user_params)
flash[:notice] = "Profile was successfully updated"
else
- flash[:alert] = "Failed to update profile"
+ messages = @user.errors.full_messages.uniq.join('. ')
+ flash[:alert] = "Failed to update profile. #{messages}"
end
respond_to do |format|
@@ -43,7 +42,7 @@ class ProfilesController < ApplicationController
end
def history
- @events = current_user.recent_events.page(params[:page]).per(20)
+ @events = current_user.recent_events.page(params[:page]).per(PER_PAGE)
end
def update_username
@@ -66,9 +65,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
+ :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/application_controller.rb b/app/controllers/projects/application_controller.rb
index 4719933394f..ee88d49b400 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -1,7 +1,7 @@
class Projects::ApplicationController < ApplicationController
- before_filter :project
- before_filter :repository
- layout :determine_layout
+ before_action :project
+ before_action :repository
+ layout 'project'
def authenticate_user!
# Restrict access to Projects area only
@@ -17,14 +17,6 @@ class Projects::ApplicationController < ApplicationController
super
end
- def determine_layout
- if current_user
- 'projects'
- else
- 'public_projects'
- end
- end
-
def require_branch_head
unless @repository.branch_names.include?(@ref)
redirect_to(
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index a482b90880d..9c3763d5934 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -1,7 +1,5 @@
class Projects::AvatarsController < Projects::ApplicationController
- layout 'project'
-
- before_filter :project
+ before_action :project
def show
@blob = @project.repository.blob_at_branch('master', @project.avatar_in_git)
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 489a6ae5666..3362264dcce 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -2,12 +2,12 @@
class Projects::BlameController < Projects::ApplicationController
include ExtractsPath
- before_filter :require_non_empty_project
- before_filter :assign_ref_vars
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
def show
- @blob = @repository.blob_at(@commit.id, @path)
- @blame = Gitlab::Git::Blame.new(project.repository, @commit.id, @path)
+ @blame = Gitlab::Git::Blame.new(@repository, @commit.id, @path)
+ @blob = @blame.blob
end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 4b7eb4df298..b762518d377 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -6,15 +6,15 @@ class Projects::BlobController < Projects::ApplicationController
# Raised when given an invalid file path
class InvalidPathError < StandardError; end
- before_filter :require_non_empty_project, except: [:new, :create]
- before_filter :authorize_download_code!
- before_filter :authorize_push_code!, only: [:destroy]
- before_filter :assign_blob_vars
- before_filter :commit, except: [:new, :create]
- before_filter :blob, except: [:new, :create]
- before_filter :from_merge_request, only: [:edit, :update]
- before_filter :after_edit_path, only: [:edit, :update]
- before_filter :require_branch_head, only: [:edit, :update]
+ before_action :require_non_empty_project, except: [:new, :create]
+ before_action :authorize_download_code!
+ before_action :authorize_push_code!, only: [:destroy]
+ before_action :assign_blob_vars
+ before_action :commit, except: [:new, :create]
+ before_action :blob, except: [:new, :create]
+ before_action :from_merge_request, only: [:edit, :update]
+ before_action :after_edit_path, only: [:edit, :update]
+ before_action :require_branch_head, only: [:edit, :update]
def new
commit unless @repository.empty?
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 690501f3060..696011b94b9 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -1,14 +1,14 @@
class Projects::BranchesController < Projects::ApplicationController
include ActionView::Helpers::SanitizeHelper
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_download_code!
- before_filter :authorize_push_code!, only: [:create, :destroy]
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
+ before_action :authorize_push_code!, only: [:create, :destroy]
def index
@sort = params[:sort] || 'name'
@branches = @repository.branches_sorted_by(@sort)
- @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(30)
+ @branches = Kaminari.paginate_array(@branches).page(params[:page]).per(PER_PAGE)
end
def recent
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 87e39f1363a..78d42d695b6 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -3,18 +3,18 @@
# Not to be confused with CommitsController, plural.
class Projects::CommitController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_download_code!
- before_filter :commit
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
+ before_action :commit
def show
return git_not_found! unless @commit
- @line_notes = @project.notes.for_commit_id(commit.id).inline
+ @line_notes = commit.notes.inline
@diffs = @commit.diffs
@note = @project.build_commit_note(commit)
- @notes_count = @project.notes.for_commit_id(commit.id).count
- @notes = @project.notes.for_commit_id(@commit.id).not_inline.fresh
+ @notes_count = commit.notes.count
+ @notes = commit.notes.not_inline.fresh
@noteable = @commit
@comments_allowed = @reply_allowed = true
@comments_target = {
@@ -36,6 +36,6 @@ class Projects::CommitController < Projects::ApplicationController
end
def commit
- @commit ||= @project.repository.commit(params[:id])
+ @commit ||= @project.commit(params[:id])
end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 4b6ab437476..d1c15174aea 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -3,9 +3,9 @@ require "base64"
class Projects::CommitsController < Projects::ApplicationController
include ExtractsPath
- before_filter :require_non_empty_project
- before_filter :assign_ref_vars
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
def show
@repo = @project.repository
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 146808fa562..7c20b81c0b1 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -1,14 +1,16 @@
+require 'addressable/uri'
+
class Projects::CompareController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
def index
end
def show
- base_ref = params[:from]
- head_ref = params[:to]
+ base_ref = Addressable::URI.unescape(params[:from])
+ head_ref = Addressable::URI.unescape(params[:to])
compare_result = CompareService.new.execute(
current_user,
diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb
index b7cc305899c..8c1bbf76917 100644
--- a/app/controllers/projects/deploy_keys_controller.rb
+++ b/app/controllers/projects/deploy_keys_controller.rb
@@ -2,13 +2,20 @@ class Projects::DeployKeysController < Projects::ApplicationController
respond_to :html
# Authorize
- before_filter :authorize_admin_project!
+ before_action :authorize_admin_project!
layout "project_settings"
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,34 +39,24 @@ 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 project_deploy_keys_url }
- 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)
end
def disable
- @project.deploy_keys_projects.where(deploy_key_id: params[:id]).last.destroy
+ @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/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 21a151a426e..9e72597ea87 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -1,7 +1,7 @@
class Projects::ForksController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
def new
@namespaces = current_user.manageable_namespaces
@@ -18,7 +18,6 @@ class Projects::ForksController < Projects::ApplicationController
notice: 'Project was successfully forked.'
)
else
- @title = 'Fork project'
render :error
end
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 752474b4a4c..a060ea6f998 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -1,7 +1,7 @@
class Projects::GraphsController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
def show
respond_to do |format|
@@ -28,8 +28,8 @@ class Projects::GraphsController < Projects::ApplicationController
@commits.each do |commit|
@log << {
- author_name: commit.author_name.force_encoding('UTF-8'),
- author_email: commit.author_email.force_encoding('UTF-8'),
+ author_name: commit.author_name,
+ author_email: commit.author_email,
date: commit.committed_date.strftime("%Y-%m-%d")
}
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index ba95bb13e1f..57fc48ac7da 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,6 +1,6 @@
class Projects::HooksController < Projects::ApplicationController
# Authorize
- before_filter :authorize_admin_project!
+ before_action :authorize_admin_project!
respond_to :html
diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb
index 79d9910ce87..066b66014f8 100644
--- a/app/controllers/projects/imports_controller.rb
+++ b/app/controllers/projects/imports_controller.rb
@@ -1,8 +1,8 @@
class Projects::ImportsController < Projects::ApplicationController
# Authorize
- before_filter :authorize_admin_project!
- before_filter :require_no_repo
- before_filter :redirect_if_progress, except: :show
+ before_action :authorize_admin_project!
+ before_action :require_no_repo
+ before_action :redirect_if_progress, except: :show
def new
end
@@ -37,7 +37,7 @@ class Projects::ImportsController < Projects::ApplicationController
private
def require_no_repo
- if @project.repository_exists?
+ if @project.repository_exists? && !@project.import_in_progress?
redirect_to(namespace_project_path(@project.namespace, @project)) and return
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 1f1a9b4d43a..c524e1a0ea3 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -1,18 +1,18 @@
class Projects::IssuesController < Projects::ApplicationController
- before_filter :module_enabled
- before_filter :issue, only: [:edit, :update, :show]
+ before_action :module_enabled
+ before_action :issue, only: [:edit, :update, :show, :toggle_subscription]
# Allow read any issue
- before_filter :authorize_read_issue!
+ before_action :authorize_read_issue!
# Allow write(create) issue
- before_filter :authorize_write_issue!, only: [:new, :create]
+ before_action :authorize_write_issue!, only: [:new, :create]
# Allow modify issue
- before_filter :authorize_modify_issue!, only: [:edit, :update]
+ before_action :authorize_modify_issue!, only: [:edit, :update]
# Allow issues bulk update
- before_filter :authorize_admin_issues!, only: [:bulk_update]
+ before_action :authorize_admin_issues!, only: [:bulk_update]
respond_to :html
@@ -20,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
terms = params['issue_search']
@issues = get_issues_collection
@issues = @issues.full_search(terms) if terms.present?
- @issues = @issues.page(params[:page]).per(20)
+ @issues = @issues.page(params[:page]).per(PER_PAGE)
respond_to do |format|
format.html
@@ -97,6 +97,12 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_to :back, notice: "#{result[:count]} issues updated"
end
+ def toggle_subscription
+ @issue.toggle_subscription(current_user)
+
+ render nothing: true
+ end
+
protected
def issue
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 5e31fce4b0e..2f8cb203cf9 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -1,13 +1,13 @@
class Projects::LabelsController < Projects::ApplicationController
- before_filter :module_enabled
- before_filter :label, only: [:edit, :update, :destroy]
- before_filter :authorize_labels!
- before_filter :authorize_admin_labels!, except: [:index]
+ before_action :module_enabled
+ before_action :label, only: [:edit, :update, :destroy]
+ before_action :authorize_labels!
+ before_action :authorize_admin_labels!, except: [:index]
respond_to :js, :html
def index
- @labels = @project.labels.page(params[:page]).per(20)
+ @labels = @project.labels.page(params[:page]).per(PER_PAGE)
end
def new
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 848cf367493..5b93e95866a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -1,24 +1,35 @@
require 'gitlab/satellite/satellite'
class Projects::MergeRequestsController < Projects::ApplicationController
- before_filter :module_enabled
- before_filter :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status]
- before_filter :closes_issues, only: [:edit, :update, :show, :diffs]
- before_filter :validates_merge_request, only: [:show, :diffs]
- before_filter :define_show_vars, only: [:show, :diffs]
+ before_action :module_enabled
+ before_action :merge_request, only: [:edit, :update, :show, :diffs, :automerge, :automerge_check, :ci_status, :toggle_subscription]
+ before_action :closes_issues, only: [:edit, :update, :show, :diffs]
+ before_action :validates_merge_request, only: [:show, :diffs]
+ before_action :define_show_vars, only: [:show, :diffs]
# Allow read any merge_request
- before_filter :authorize_read_merge_request!
+ before_action :authorize_read_merge_request!
# Allow write(create) merge_request
- before_filter :authorize_write_merge_request!, only: [:new, :create]
+ before_action :authorize_write_merge_request!, only: [:new, :create]
# Allow modify merge_request
- before_filter :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort]
+ before_action :authorize_modify_merge_request!, only: [:close, :edit, :update, :sort]
def index
+ terms = params['issue_search']
@merge_requests = get_merge_requests_collection
- @merge_requests = @merge_requests.page(params[:page]).per(20)
+ @merge_requests = @merge_requests.full_search(terms) if terms.present?
+ @merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: {
+ html: view_to_html_string("projects/merge_requests/_merge_requests")
+ }
+ end
+ end
end
def show
@@ -78,10 +89,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request = MergeRequests::CreateService.new(project, current_user, merge_request_params).execute
if @merge_request.valid?
- redirect_to(
- merge_request_path(@merge_request),
- notice: 'Merge request was successfully created.'
- )
+ redirect_to(merge_request_path(@merge_request))
else
@source_project = @merge_request.source_project
@target_project = @merge_request.target_project
@@ -97,8 +105,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.js
format.html do
redirect_to([@merge_request.target_project.namespace.becomes(Namespace),
- @merge_request.target_project, @merge_request],
- notice: 'Merge request was successfully updated.')
+ @merge_request.target_project, @merge_request])
end
format.json do
render json: {
@@ -117,13 +124,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.check_if_can_be_merged
end
- render json: { merge_status: @merge_request.merge_status_name }
+ render json: { merge_status: @merge_request.automerge_status }
end
def automerge
return access_denied! unless allowed_to_merge?
- if @merge_request.open? && @merge_request.can_be_merged?
+ if @merge_request.automergeable?
AutoMergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = true
else
@@ -139,7 +146,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def branch_to
@target_project = selected_target_project
- @commit = @target_project.repository.commit(params[:ref]) if params[:ref].present?
+ @commit = @target_project.commit(params[:ref]) if params[:ref].present?
end
def update_branches
@@ -153,10 +160,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def ci_status
ci_service = @merge_request.source_project.ci_service
- status = ci_service.commit_status(merge_request.last_commit.sha)
+ status = ci_service.commit_status(merge_request.last_commit.sha, merge_request.source_branch)
if ci_service.respond_to?(:commit_coverage)
- coverage = ci_service.commit_coverage(merge_request.last_commit.sha)
+ coverage = ci_service.commit_coverage(merge_request.last_commit.sha, merge_request.source_branch)
end
response = {
@@ -167,6 +174,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
render json: response
end
+ def toggle_subscription
+ @merge_request.toggle_subscription(current_user)
+
+ render nothing: true
+ end
+
protected
def selected_target_project
@@ -244,7 +257,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def allowed_to_push_code?(project, branch)
- ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, branch)
+ ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch)
end
def merge_request_params
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index afdb560e73c..61689488d13 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -1,12 +1,12 @@
class Projects::MilestonesController < Projects::ApplicationController
- before_filter :module_enabled
- before_filter :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests]
+ before_action :module_enabled
+ before_action :milestone, only: [:edit, :update, :destroy, :show, :sort_issues, :sort_merge_requests]
# Allow read any milestone
- before_filter :authorize_read_milestone!
+ before_action :authorize_read_milestone!
# Allow admin milestone
- before_filter :authorize_admin_milestone!, except: [:index, :show]
+ before_action :authorize_admin_milestone!, except: [:index, :show]
respond_to :html
@@ -18,7 +18,7 @@ class Projects::MilestonesController < Projects::ApplicationController
end
@milestones = @milestones.includes(:project)
- @milestones = @milestones.page(params[:page]).per(20)
+ @milestones = @milestones.page(params[:page]).per(PER_PAGE)
end
def new
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 83d1c1dacae..06aef91cadd 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -2,9 +2,9 @@ class Projects::NetworkController < Projects::ApplicationController
include ExtractsPath
include ApplicationHelper
- before_filter :require_non_empty_project
- before_filter :assign_ref_vars
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
def show
respond_to do |format|
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 868629a0bc4..496b85cb46d 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,9 +1,9 @@
class Projects::NotesController < Projects::ApplicationController
# Authorize
- before_filter :authorize_read_note!
- before_filter :authorize_write_note!, only: [:create]
- before_filter :authorize_admin_note!, only: [:update, :destroy]
- before_filter :find_current_user_notes, except: [:destroy, :delete_attachment]
+ before_action :authorize_read_note!
+ before_action :authorize_write_note!, only: [:create]
+ before_action :authorize_admin_note!, only: [:update, :destroy]
+ before_action :find_current_user_notes, except: [:destroy, :delete_attachment]
def index
current_fetched_at = Time.now.to_i
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
new file mode 100644
index 00000000000..d7fbc979067
--- /dev/null
+++ b/app/controllers/projects/project_members_controller.rb
@@ -0,0 +1,98 @@
+class Projects::ProjectMembersController < Projects::ApplicationController
+ # Authorize
+ before_action :authorize_admin_project!, except: :leave
+
+ layout "project_settings"
+
+ 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
+ @project_members = @project_members.where(user_id: users)
+ end
+
+ @project_members = @project_members.order('access_level DESC')
+
+ @group = @project.group
+ if @group
+ @group_members = @group.group_members
+ @group_members = @group_members.non_invite unless can?(current_user, :admin_group, @group)
+
+ if params[:search].present?
+ users = @group.users.search(params[:search]).to_a
+ @group_members = @group_members.where(user_id: users)
+ end
+
+ @group_members = @group_members.order('access_level DESC').limit(20)
+ end
+
+ @project_member = @project.project_members.new
+ end
+
+ def new
+ @project_member = @project.project_members.new
+ end
+
+ def create
+ @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+
+ redirect_to namespace_project_project_members_path(@project.namespace, @project)
+ end
+
+ def update
+ @project_member = @project.project_members.find(params[:id])
+ @project_member.update_attributes(member_params)
+ end
+
+ def destroy
+ @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)
+ 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
+
+ respond_to do |format|
+ format.html { redirect_to :back }
+ format.js { render nothing: true }
+ end
+ end
+
+ def apply_import
+ giver = Project.find(params[:source_project_id])
+ status = @project.team.import(giver, current_user)
+ notice = status ? "Successfully imported" : "Import failed"
+
+ redirect_to(namespace_project_project_members_path(project.namespace, project),
+ notice: notice)
+ end
+
+ protected
+
+ def member_params
+ params.require(:project_member).permit(:user_id, :access_level)
+ end
+end
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index ac36ac6fcd3..6b52eccebf7 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,7 +1,7 @@
class Projects::ProtectedBranchesController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_admin_project!
+ before_action :require_non_empty_project
+ before_action :authorize_admin_project!
layout "project_settings"
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index b1a029ce696..647c1454078 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -2,9 +2,9 @@
class Projects::RawController < Projects::ApplicationController
include ExtractsPath
- before_filter :require_non_empty_project
- before_filter :assign_ref_vars
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
def show
@blob = @repository.blob_at(@commit.id, @path)
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 67acf45ab7f..01ca1537c0e 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -1,9 +1,9 @@
class Projects::RefsController < Projects::ApplicationController
include ExtractsPath
- before_filter :require_non_empty_project
- before_filter :assign_ref_vars
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
def switch
respond_to do |format|
@@ -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..c4a5e2d6359 100644
--- a/app/controllers/projects/repositories_controller.rb
+++ b/app/controllers/projects/repositories_controller.rb
@@ -1,8 +1,8 @@
class Projects::RepositoriesController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project, except: :create
- before_filter :authorize_download_code!
- before_filter :authorize_admin_project!, only: :create
+ before_action :require_non_empty_project, except: :create
+ before_action :authorize_download_code!
+ before_action :authorize_admin_project!, only: :create
def create
@project.create_repository
@@ -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/services_controller.rb b/app/controllers/projects/services_controller.rb
index 570447c746c..dc18bbd8d5b 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -1,7 +1,16 @@
class Projects::ServicesController < Projects::ApplicationController
+ ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain,
+ :room, :recipients, :project_url, :webhook,
+ :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
+ :build_key, :server, :teamcity_url, :build_type,
+ :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
+ :colorize_messages, :channels,
+ :push_events, :issues_events, :merge_requests_events, :tag_push_events,
+ :note_events, :send_from_committer_email, :disable_diffs, :external_wiki_url,
+ :notify, :color]
# Authorize
- before_filter :authorize_admin_project!
- before_filter :service, only: [:edit, :update, :test]
+ before_action :authorize_admin_project!
+ before_action :service, only: [:edit, :update, :test]
respond_to :html
@@ -45,15 +54,6 @@ class Projects::ServicesController < Projects::ApplicationController
end
def service_params
- params.require(:service).permit(
- :title, :token, :type, :active, :api_key, :subdomain,
- :room, :recipients, :project_url, :webhook,
- :user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :build_type,
- :description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
- :colorize_messages, :channels,
- :push_events, :issues_events, :merge_requests_events, :tag_push_events,
- :note_events, :send_from_committer_email, :disable_diffs
- )
+ params.require(:service).permit(ALLOWED_PARAMS)
end
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 6c250e4ffed..3d75abcc29d 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -1,18 +1,18 @@
class Projects::SnippetsController < Projects::ApplicationController
- before_filter :module_enabled
- before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :module_enabled
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read any snippet
- before_filter :authorize_read_project_snippet!
+ before_action :authorize_read_project_snippet!
# Allow write(create) snippet
- before_filter :authorize_write_project_snippet!, only: [:new, :create]
+ before_action :authorize_write_project_snippet!, only: [:new, :create]
# Allow modify snippet
- before_filter :authorize_modify_project_snippet!, only: [:edit, :update]
+ before_action :authorize_modify_project_snippet!, only: [:edit, :update]
# Allow destroy snippet
- before_filter :authorize_admin_project_snippet!, only: [:destroy]
+ before_action :authorize_admin_project_snippet!, only: [:destroy]
respond_to :html
@@ -28,26 +28,22 @@ class Projects::SnippetsController < Projects::ApplicationController
end
def create
- @snippet = @project.snippets.build(snippet_params)
- @snippet.author = current_user
-
- if @snippet.save
- redirect_to namespace_project_snippet_path(@project.namespace, @project,
- @snippet)
- else
- respond_with(@snippet)
- end
+ @snippet = CreateSnippetService.new(@project, current_user,
+ snippet_params).execute
+ respond_with(@snippet,
+ location: namespace_project_snippet_path(@project.namespace,
+ @project, @snippet))
end
def edit
end
def update
- if @snippet.update_attributes(snippet_params)
- redirect_to namespace_project_snippet_path(@project.namespace, @project, @snippet)
- else
- respond_with(@snippet)
- end
+ UpdateSnippetService.new(project, current_user, @snippet,
+ snippet_params).execute
+ respond_with(@snippet,
+ location: namespace_project_snippet_path(@project.namespace,
+ @project, @snippet))
end
def show
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 08c7ce3f37d..f565fbbbbc3 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -1,13 +1,13 @@
class Projects::TagsController < Projects::ApplicationController
# Authorize
- before_filter :require_non_empty_project
- before_filter :authorize_download_code!
- before_filter :authorize_push_code!, only: [:create]
- before_filter :authorize_admin_project!, only: [:destroy]
+ before_action :require_non_empty_project
+ before_action :authorize_download_code!
+ before_action :authorize_push_code!, only: [:create]
+ before_action :authorize_admin_project!, only: [:destroy]
def index
sorted = VersionSorter.rsort(@repository.tag_names)
- @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(30)
+ @tags = Kaminari.paginate_array(sorted).page(params[:page]).per(PER_PAGE)
end
def create
@@ -24,14 +24,13 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
- tag = @repository.find_tag(params[:id])
-
- if tag && @repository.rm_tag(tag.name)
- EventCreateService.new.push_ref(@project, current_user, tag, 'rm', 'refs/tags')
- end
+ DeleteTagService.new(project, current_user).execute(params[:id])
respond_to do |format|
- format.html { redirect_to namespace_project_tags_path }
+ format.html do
+ redirect_to namespace_project_tags_path(@project.namespace,
+ @project)
+ end
format.js
end
end
diff --git a/app/controllers/projects/team_members_controller.rb b/app/controllers/projects/team_members_controller.rb
deleted file mode 100644
index f8a248ed729..00000000000
--- a/app/controllers/projects/team_members_controller.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-class Projects::TeamMembersController < Projects::ApplicationController
- # Authorize
- before_filter :authorize_admin_project!, except: :leave
-
- layout "project_settings"
-
- def index
- @group = @project.group
- @project_members = @project.project_members.order('access_level DESC')
- end
-
- def new
- @user_project_relation = @project.project_members.new
- end
-
- def create
- users = User.where(id: params[:user_ids].split(','))
- @project.team << [users, params[:access_level]]
-
- redirect_to namespace_project_team_index_path(@project.namespace, @project)
- end
-
- def update
- @user_project_relation = @project.project_members.find_by(user_id: member)
- @user_project_relation.update_attributes(member_params)
-
- unless @user_project_relation.valid?
- flash[:alert] = "User should have at least one role"
- end
- redirect_to namespace_project_team_index_path(@project.namespace, @project)
- end
-
- def destroy
- @user_project_relation = @project.project_members.find_by(user_id: member)
- @user_project_relation.destroy
-
- respond_to do |format|
- format.html do
- redirect_to namespace_project_team_index_path(@project.namespace,
- @project)
- end
- format.js { render nothing: true }
- end
- end
-
- def leave
- @project.project_members.find_by(user_id: current_user).destroy
-
- respond_to do |format|
- format.html { redirect_to :back }
- format.js { render nothing: true }
- end
- end
-
- def apply_import
- giver = Project.find(params[:source_project_id])
- status = @project.team.import(giver)
- notice = status ? "Successfully imported" : "Import failed"
-
- redirect_to(namespace_project_team_index_path(project.namespace, project),
- notice: notice)
- end
-
- protected
-
- def member
- @member ||= User.find_by(username: params[:id])
- end
-
- def member_params
- params.require(:project_member).permit(:user_id, :access_level)
- end
-end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index b23010bf595..b659e15f242 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -2,9 +2,9 @@
class Projects::TreeController < Projects::ApplicationController
include ExtractsPath
- before_filter :require_non_empty_project, except: [:new, :create]
- before_filter :assign_ref_vars
- before_filter :authorize_download_code!
+ before_action :require_non_empty_project, except: [:new, :create]
+ before_action :assign_ref_vars
+ before_action :authorize_download_code!
def show
if tree.entries.empty?
diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb
index 9020e86c44e..71ecc20dd95 100644
--- a/app/controllers/projects/uploads_controller.rb
+++ b/app/controllers/projects/uploads_controller.rb
@@ -1,7 +1,6 @@
class Projects::UploadsController < Projects::ApplicationController
- layout 'project'
-
- before_filter :project
+ skip_before_action :authenticate_user!, :reject_blocked!, :project,
+ :repository, if: -> { action_name == 'show' && image? }
def create
link_to_file = ::Projects::UploadService.new(project, params[:file]).
@@ -21,15 +20,32 @@ class Projects::UploadsController < Projects::ApplicationController
end
def show
- uploader = FileUploader.new(project, params[:secret])
+ return not_found! if uploader.nil? || !uploader.file.exists?
- return redirect_to uploader.url unless uploader.file_storage?
+ disposition = uploader.image? ? 'inline' : 'attachment'
+ send_file uploader.file.path, disposition: disposition
+ end
- uploader.retrieve_from_store!(params[:filename])
+ def uploader
+ return @uploader if defined?(@uploader)
- return not_found! unless uploader.file.exists?
+ namespace = params[:namespace_id]
+ id = params[:project_id]
- disposition = uploader.image? ? 'inline' : 'attachment'
- send_file uploader.file.path, disposition: disposition
+ file_project = Project.find_with_namespace("#{namespace}/#{id}")
+
+ if file_project.nil?
+ @uploader = nil
+ return
+ end
+
+ @uploader = FileUploader.new(file_project, params[:secret])
+ @uploader.retrieve_from_store!(params[:filename])
+
+ @uploader
+ end
+
+ def image?
+ uploader && uploader.file.exists? && uploader.image?
end
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 3392fbca91e..36ef86e1909 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -1,13 +1,14 @@
require 'project_wiki'
class Projects::WikisController < Projects::ApplicationController
- before_filter :authorize_read_wiki!
- before_filter :authorize_write_wiki!, only: [:edit, :create, :history]
- before_filter :authorize_admin_wiki!, only: :destroy
- before_filter :load_project_wiki
+ before_action :authorize_read_wiki!
+ before_action :authorize_write_wiki!, only: [:edit, :create, :history]
+ before_action :authorize_admin_wiki!, only: :destroy
+ before_action :load_project_wiki
+ include WikiHelper
def pages
- @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(30)
+ @wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
end
def show
@@ -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/projects_controller.rb b/app/controllers/projects_controller.rb
index 82b8a1cc13a..dc430351551 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,22 +1,21 @@
class ProjectsController < ApplicationController
prepend_before_filter :render_go_import, only: [:show]
- skip_before_filter :authenticate_user!, only: [:show]
- before_filter :project, except: [:new, :create]
- before_filter :repository, except: [:new, :create]
+ skip_before_action :authenticate_user!, only: [:show]
+ before_action :project, except: [:new, :create]
+ before_action :repository, except: [:new, :create]
# Authorize
- before_filter :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive]
- before_filter :set_title, only: [:new, :create]
- before_filter :event_filter, only: :show
+ before_action :authorize_admin_project!, only: [:edit, :update, :destroy, :transfer, :archive, :unarchive]
+ before_action :event_filter, only: :show
- layout 'navless', only: [:new, :create, :fork]
+ layout :determine_layout
def new
@project = Project.new
end
def edit
- render 'edit', layout: 'project_settings'
+ render 'edit'
end
def create
@@ -46,7 +45,7 @@ class ProjectsController < ApplicationController
end
format.js
else
- format.html { render 'edit', layout: 'project_settings' }
+ format.html { render 'edit' }
format.js
end
end
@@ -66,30 +65,31 @@ class ProjectsController < ApplicationController
return
end
- limit = (params[:limit] || 20).to_i
-
@show_star = !(current_user && current_user.starred?(@project))
respond_to do |format|
format.html do
if @project.repository_exists?
if @project.empty_repo?
- render 'projects/empty', layout: user_layout
+ render 'projects/empty'
else
@last_push = current_user.recent_push(@project.id) if current_user
- render :show, layout: user_layout
+ render :show
end
else
- render 'projects/no_repo', layout: user_layout
+ render 'projects/no_repo'
end
end
format.json do
- @events = @project.events.recent
- @events = event_filter.apply_filter(@events).with_associations
- @events = @events.limit(limit).offset(params[:offset] || 0)
+ load_events
pager_json('events/_events', @events.count)
end
+
+ format.atom do
+ load_events
+ render layout: false
+ end
end
end
@@ -105,7 +105,7 @@ class ProjectsController < ApplicationController
if request.referer.include?('/admin')
redirect_to admin_namespaces_projects_path
else
- redirect_to projects_dashboard_path
+ redirect_to dashboard_path
end
end
end
@@ -159,12 +159,21 @@ class ProjectsController < ApplicationController
private
- def set_title
- @title = 'New Project'
+ def determine_layout
+ if [:new, :create].include?(action_name.to_sym)
+ 'application'
+ elsif [:edit, :update].include?(action_name.to_sym)
+ 'project_settings'
+ else
+ 'project'
+ end
end
- def user_layout
- current_user ? 'projects' : 'public_projects'
+ def load_events
+ @events = @project.events.recent
+ @events = event_filter.apply_filter(@events).with_associations
+ limit = (params[:limit] || 20).to_i
+ @events = @events.limit(limit).offset(params[:offset] || 0)
end
def project_params
@@ -176,11 +185,11 @@ class ProjectsController < ApplicationController
end
def autocomplete_emojis
- Rails.cache.fetch("autocomplete-emoji-#{Emoji::VERSION}") do
- Emoji.names.map do |e|
+ Rails.cache.fetch("autocomplete-emoji-#{Gemojione::VERSION}") do
+ Emoji.emojis.map do |name, emoji|
{
- name: e,
- path: view_context.image_url("emoji/#{e}.png")
+ name: name,
+ path: view_context.image_url("emoji/#{emoji["unicode"]}.png")
}
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 38d116a4ee3..830751a989f 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -1,5 +1,5 @@
class RegistrationsController < Devise::RegistrationsController
- before_filter :signup_enabled?
+ before_action :signup_enabled?
def new
redirect_to(new_user_session_path)
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 55926a1ed22..4e2ea6c5710 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -1,41 +1,58 @@
class SearchController < ApplicationController
include SearchHelper
+ layout 'search'
+
def show
- @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?
- @scope = params[:scope]
- @show_snippets = params[:snippets].eql? 'true'
+ return if params[:search].nil? || params[:search].blank?
- @search_results = if @project
- return access_denied! unless can?(current_user, :download_code, @project)
+ @search_term = params[:search]
- unless %w(blobs notes issues merge_requests wiki_blobs).
- include?(@scope)
- @scope = 'blobs'
- end
+ if params[:project_id].present?
+ @project = Project.find_by(id: params[:project_id])
+ @project = nil unless can?(current_user, :download_code, @project)
+ end
- Search::ProjectService.new(@project, current_user, params).execute
- elsif @show_snippets
- unless %w(snippet_blobs snippet_titles).include?(@scope)
- @scope = 'snippet_blobs'
- end
+ if params[:group_id].present?
+ @group = Group.find_by(id: params[:group_id])
+ @group = nil unless can?(current_user, :read_group, @group)
+ end
- Search::SnippetService.new(current_user, params).execute
- else
- unless %w(projects issues merge_requests).include?(@scope)
- @scope = 'projects'
- end
+ @scope = params[:scope]
+ @show_snippets = params[:snippets].eql? 'true'
- Search::GlobalService.new(current_user, params).execute
- end
+ @search_results =
+ if @project
+ unless %w(blobs notes issues merge_requests wiki_blobs).
+ include?(@scope)
+ @scope = 'blobs'
+ end
+
+ Search::ProjectService.new(@project, current_user, params).execute
+ elsif @show_snippets
+ unless %w(snippet_blobs snippet_titles).include?(@scope)
+ @scope = 'snippet_blobs'
+ end
+
+ Search::SnippetService.new(current_user, params).execute
+ else
+ unless %w(projects issues merge_requests).include?(@scope)
+ @scope = 'projects'
+ end
+ Search::GlobalService.new(current_user, params).execute
+ end
@objects = @search_results.objects(@scope, params[:page])
end
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/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 6ac048e4b83..cf672c5c093 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,39 +1,36 @@
class SnippetsController < ApplicationController
- before_filter :snippet, only: [:show, :edit, :destroy, :update, :raw]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow modify snippet
- before_filter :authorize_modify_snippet!, only: [:edit, :update]
+ before_action :authorize_modify_snippet!, only: [:edit, :update]
# Allow destroy snippet
- before_filter :authorize_admin_snippet!, only: [:destroy]
+ before_action :authorize_admin_snippet!, only: [:destroy]
- before_filter :set_title
-
- skip_before_filter :authenticate_user!, only: [:index, :user_index, :show, :raw]
+ skip_before_action :authenticate_user!, only: [:index, :user_index, :show, :raw]
+ layout 'snippets'
respond_to :html
- layout :determine_layout
-
def index
- @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(20)
- end
-
- def user_index
- @user = User.find_by(username: params[:username])
-
- render_404 and return unless @user
-
- @snippets = SnippetsFinder.new.execute(current_user, {
- filter: :by_user,
- user: @user,
- scope: params[:scope] }).
- page(params[:page]).per(20)
-
- if @user == current_user
- render 'current_user_index'
+ if params[:username].present?
+ @user = User.find_by(username: params[:username])
+
+ render_404 and return unless @user
+
+ @snippets = SnippetsFinder.new.execute(current_user, {
+ filter: :by_user,
+ user: @user,
+ scope: params[:scope] }).
+ page(params[:page]).per(PER_PAGE)
+
+ if @user == current_user
+ render 'current_user_index'
+ else
+ render 'user_index'
+ end
else
- render 'user_index'
+ @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE)
end
end
@@ -42,25 +39,19 @@ class SnippetsController < ApplicationController
end
def create
- @snippet = PersonalSnippet.new(snippet_params)
- @snippet.author = current_user
+ @snippet = CreateSnippetService.new(nil, current_user,
+ snippet_params).execute
- if @snippet.save
- redirect_to snippet_path(@snippet)
- else
- respond_with @snippet
- end
+ respond_with @snippet.becomes(Snippet)
end
def edit
end
def update
- if @snippet.update_attributes(snippet_params)
- redirect_to snippet_path(@snippet)
- else
- respond_with @snippet
- end
+ UpdateSnippetService.new(nil, current_user, @snippet,
+ snippet_params).execute
+ respond_with @snippet.becomes(Snippet)
end
def show
@@ -104,16 +95,7 @@ class SnippetsController < ApplicationController
return render_404 unless can?(current_user, :admin_personal_snippet, @snippet)
end
- def set_title
- @title = 'Snippets'
- @title_url = snippets_path
- end
-
def snippet_params
params.require(:personal_snippet).permit(:title, :content, :file_name, :private, :visibility_level)
end
-
- def determine_layout
- current_user ? 'navless' : 'public_users'
- end
end
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 810ac9f34bd..17edff68be2 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -1,24 +1,15 @@
class UploadsController < ApplicationController
- skip_before_filter :authenticate_user!, :reject_blocked!
- before_filter :authorize_access
+ skip_before_action :authenticate_user!
+ before_action :find_model, :authorize_access!
def show
- unless upload_model && upload_mount
- return not_found!
- end
-
- model = upload_model.find(params[:id])
- uploader = model.send(upload_mount)
-
- if model.respond_to?(:project) && !can?(current_user, :read_project, model.project)
- return not_found!
- end
+ uploader = @model.send(upload_mount)
unless uploader.file_storage?
return redirect_to uploader.url
end
- unless uploader.file.exists?
+ unless uploader.file && uploader.file.exists?
return not_found!
end
@@ -28,9 +19,34 @@ class UploadsController < ApplicationController
private
- def authorize_access
- unless params[:mounted_as] == 'avatar'
- authenticate_user! && reject_blocked!
+ def find_model
+ unless upload_model && upload_mount
+ return not_found!
+ end
+
+ @model = upload_model.find(params[:id])
+ end
+
+ def authorize_access!
+ authorized =
+ case @model
+ when Project
+ can?(current_user, :read_project, @model)
+ when Group
+ can?(current_user, :read_group, @model)
+ when Note
+ can?(current_user, :read_project, @model.project)
+ else
+ # No authentication required for user avatars.
+ true
+ end
+
+ return if authorized
+
+ if current_user
+ not_found!
+ else
+ authenticate_user!
end
end
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 8a13394dbac..2bb5c338cf6 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1,13 +1,9 @@
class UsersController < ApplicationController
- skip_before_filter :authenticate_user!
- before_filter :set_user
- layout :determine_layout
+ skip_before_action :authenticate_user!
+ before_action :set_user
def show
- @contributed_projects = Project.
- where(id: authorized_projects_ids & @user.contributed_projects_ids).
- in_group_namespace.
- includes(:namespace).
+ @contributed_projects = contributed_projects.joined(@user).
reject(&:forked?)
@projects = @user.personal_projects.
@@ -16,23 +12,23 @@ class UsersController < ApplicationController
# Collect only groups common for both users
@groups = @user.groups & GroupsFinder.new.execute(current_user)
- # Get user activity feed for projects common for both users
- @events = @user.recent_events.
- where(project_id: authorized_projects_ids).
- with_associations.limit(30)
-
- @title = @user.name
- @title_url = user_path(@user)
-
respond_to do |format|
format.html
- format.atom { render layout: false }
+
+ format.atom do
+ load_events
+ render layout: false
+ end
+
+ format.json do
+ load_events
+ pager_json("events/_events", @events.count)
+ end
end
end
def calendar
- projects = Project.where(id: authorized_projects_ids & @user.contributed_projects_ids)
- calendar = Gitlab::CommitsCalendar.new(projects, @user)
+ calendar = contributions_calendar
@timestamps = calendar.timestamps
@starting_year = calendar.starting_year
@starting_month = calendar.starting_month
@@ -40,12 +36,15 @@ class UsersController < ApplicationController
render 'calendar', layout: false
end
- def determine_layout
- if current_user
- 'navless'
- else
- 'public_users'
+ def calendar_activities
+ @calendar_date = Date.parse(params[:date]) rescue nil
+ @events = []
+
+ if @calendar_date
+ @events = contributions_calendar.events_by_date(@calendar_date)
end
+
+ render 'calendar_activities', layout: false
end
private
@@ -63,4 +62,24 @@ class UsersController < ApplicationController
@authorized_projects_ids ||=
ProjectsFinder.new.execute(current_user).pluck(:id)
end
+
+ def contributed_projects
+ @contributed_projects = Project.
+ where(id: authorized_projects_ids & @user.contributed_projects_ids).
+ includes(:namespace)
+ end
+
+ def contributions_calendar
+ @contributions_calendar ||= Gitlab::ContributionsCalendar.
+ new(contributed_projects.reject(&:forked?), @user)
+ end
+
+ def load_events
+ # Get user activity feed for projects common for both users
+ @events = @user.recent_events.
+ where(project_id: authorized_projects_ids).
+ with_associations
+
+ @events = @events.limit(20).offset(params[:offset] || 0)
+ end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 088a766ed3a..b8f367c6339 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)
@@ -111,8 +113,9 @@ class IssuableFinder
end
def by_milestone(items)
- if params[:milestone_id].present?
- items = items.where(milestone_id: (params[:milestone_id] == '0' ? nil : params[:milestone_id]))
+ if params[:milestone_title].present?
+ milestone_ids = (params[:milestone_title] == NONE ? nil : Milestone.where(title: params[:milestone_title]).pluck(:id))
+ items = items.where(milestone_id: milestone_ids)
end
items
@@ -120,7 +123,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 +131,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 a81e41819b7..6e86400a4f6 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -86,15 +86,6 @@ module ApplicationHelper
end
end
- def group_icon(group_path)
- group = Group.find_by(path: group_path)
- if group && group.avatar.present?
- group.avatar.url
- else
- image_path('no_group_avatar.png')
- end
- end
-
def avatar_icon(user_email = '', size = nil)
user = User.find_by(email: user_email)
@@ -134,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
@@ -183,16 +174,10 @@ module ApplicationHelper
Digest::SHA1.hexdigest string
end
- def authbutton(provider, size = 64)
- file_name = "#{provider.to_s.split('_').first}_#{size}.png"
- image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}")
- end
-
def simple_sanitize(str)
sanitize(str, tags: %w(a span))
end
-
def body_data_page
path = controller.controller_path.split('/')
namespace = path.first if path.second
@@ -251,31 +236,37 @@ 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
+ # 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
- # 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'
+ 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 = {})
+ begin
+ 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
+ rescue URI::Error
+ # noop
end
- super
+ html_options
end
def escaped_autolink(text)
@@ -290,7 +281,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],
@@ -303,6 +296,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
@@ -320,11 +319,17 @@ module ApplicationHelper
end
end
- def nav_sidebar_class
- if nav_menu_collapsed?
- "page-sidebar-collapsed"
- else
- "page-sidebar-expanded"
- end
+ def state_filters_text_for(entity, project)
+ entity_title = entity.to_s.humanize
+
+ count =
+ if project.nil?
+ ""
+ elsif current_controller?(:issues)
+ " (#{project.issues.send(entity).count})"
+ elsif current_controller?(:merge_requests)
+ " (#{project.merge_requests.send(entity).count})"
+ end
+ "#{entity_title}#{count}"
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 1ee086da997..241d6075c9f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -18,4 +18,21 @@ module ApplicationSettingsHelper
def extra_sign_in_text
current_application_settings.sign_in_text
end
+
+ # Return a group of checkboxes that use Bootstrap's button plugin for a
+ # toggle button effect.
+ def restricted_level_checkboxes(help_block_id)
+ Gitlab::VisibilityLevel.options.map do |name, level|
+ checked = restricted_visibility_levels(true).include?(level)
+ css_class = 'btn btn-primary'
+ css_class += ' active' if checked
+ checkbox_name = 'application_setting[restricted_visibility_levels][]'
+
+ label_tag(checkbox_name, class: css_class) do
+ check_box_tag(checkbox_name, level, checked,
+ autocomplete: 'off',
+ 'aria-describedby' => help_block_id) + name
+ end
+ end
+ end
end
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/branches_helper.rb b/app/helpers/branches_helper.rb
index 4a5edf6d101..d6eaa7d57bc 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -12,6 +12,6 @@ module BranchesHelper
def can_push_branch?(project, branch_name)
return false unless project.repository.branch_names.include?(branch_name)
- ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, branch_name)
+ ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
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/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index 4dae96644c8..c25b54eadc6 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -1,20 +1,4 @@
module DashboardHelper
- def projects_dashboard_filter_path(options={})
- exist_opts = {
- sort: params[:sort],
- scope: params[:scope],
- group: params[:group],
- tag: params[:tag],
- visibility_level: params[:visibility_level],
- }
-
- options = exist_opts.merge(options)
-
- path = request.path
- path << "?#{options.to_param}"
- path
- end
-
def assigned_issues_dashboard_path
issues_dashboard_path(assignee_id: current_user.id)
end
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 8c921cba543..1b10795bb7b 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -7,14 +7,23 @@ module DiffHelper
end
end
- def safe_diff_files(diffs)
- diffs.first(allowed_diff_size).map do |diff|
- Gitlab::Diff::File.new(diff)
+ def allowed_diff_lines
+ if diff_hard_limit_enabled?
+ Commit::DIFF_HARD_LIMIT_LINES
+ else
+ Commit::DIFF_SAFE_LINES
end
end
- def show_diff_size_warning?(diffs)
- diffs.size > allowed_diff_size
+ def safe_diff_files(diffs)
+ lines = 0
+ safe_files = []
+ diffs.first(allowed_diff_size).each do |diff|
+ lines += diff.diff.lines.count
+ break if lines > allowed_diff_lines
+ safe_files << Gitlab::Diff::File.new(diff)
+ end
+ safe_files
end
def diff_hard_limit_enabled?
@@ -101,7 +110,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)
@@ -121,8 +130,10 @@ module DiffHelper
def inline_diff_btn
params_copy = params.dup
params_copy[:view] = 'inline'
+ # Always use HTML to handle case where JSON diff rendered this button
+ params_copy.delete(:format)
- link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn active' : 'btn') do
+ link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
'Inline'
end
end
@@ -130,14 +141,16 @@ module DiffHelper
def parallel_diff_btn
params_copy = params.dup
params_copy[:view] = 'parallel'
+ # Always use HTML to handle case where JSON diff rendered this button
+ params_copy.delete(:format)
- link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active' : 'btn') do
+ link_to url_for(params_copy), id: "commit-diff-viewtype", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
'Side-by-side'
end
end
- def submodule_link(blob, ref)
- tree, commit = submodule_links(blob, ref)
+ def submodule_link(blob, ref, repository = @repository)
+ tree, commit = submodule_links(blob, ref, repository)
commit_id = if commit.nil?
blob.id[0..10]
else
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 92cc9c426b8..0df3ecc90b7 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -30,12 +30,8 @@ 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
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index d38b546e1b2..18c75a8726b 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -25,12 +25,16 @@ module EventsHelper
def event_filter_link(key, tooltip)
key = key.to_s
- active = if @event_filter.active? key
- 'active'
- end
+ active = 'active' if @event_filter.active?(key)
+ link_opts = {
+ class: 'event_filter_link',
+ id: "#{key}_event_filter",
+ title: "Filter by #{tooltip.downcase}",
+ data: { toggle: 'tooltip', placement: 'top' }
+ }
content_tag :li, class: "filter_icon #{active}" do
- link_to request.path, class: 'has_tooltip event_filter_link', id: "#{key}_event_filter", 'data-original-title' => 'Filter by ' + tooltip.downcase do
+ link_to request.path, link_opts do
icon(icon_for_event[key]) + content_tag(:span, ' ' + tooltip)
end
end
@@ -96,7 +100,7 @@ module EventsHelper
end
end
elsif event.push?
- if event.push_with_commits?
+ if event.push_with_commits? && event.md_ref?
if event.commits_count > 1
namespace_project_compare_url(event.project.namespace, event.project,
from: event.commit_from, to:
@@ -166,7 +170,7 @@ module EventsHelper
def event_note(text)
text = first_line_in_markdown(text, 150)
- sanitize(text, tags: %w(a img b pre code p))
+ sanitize(text, tags: %w(a img b pre code p span))
end
def event_commit_title(message)
diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb
new file mode 100644
index 00000000000..7616fe6bad8
--- /dev/null
+++ b/app/helpers/explore_helper.rb
@@ -0,0 +1,17 @@
+module ExploreHelper
+ def explore_projects_filter_path(options={})
+ exist_opts = {
+ sort: params[:sort],
+ scope: params[:scope],
+ group: params[:group],
+ tag: params[:tag],
+ visibility_level: params[:visibility_level],
+ }
+
+ options = exist_opts.merge(options)
+
+ path = request.path
+ path << "?#{options.to_param}"
+ path
+ end
+end
diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb
new file mode 100644
index 00000000000..838b85afdfe
--- /dev/null
+++ b/app/helpers/external_wiki_helper.rb
@@ -0,0 +1,11 @@
+module ExternalWikiHelper
+ def get_project_wiki_path(project)
+ external_wiki_service = project.services.
+ select { |service| service.to_param == 'external_wiki' }.first
+ if external_wiki_service.present? && external_wiki_service.active?
+ external_wiki_service.properties['external_wiki_url']
+ else
+ namespace_project_wiki_path(project.namespace, project, :home)
+ end
+ end
+end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index ab30f498c01..24263a0f619 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)
@@ -29,25 +29,28 @@ module GitlabMarkdownHelper
end
def markdown(text, options={})
- unless (@markdown and options == @options)
+ unless @markdown && options == @options
@options = options
- gitlab_renderer = Redcarpet::Render::GitlabHTML.new(self, {
- # see https://github.com/vmg/redcarpet#darling-i-packed-you-a-couple-renderers-for-lunch-
- filter_html: true,
- 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, {
+ # Handled further down the line by Gitlab::Markdown::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,
+ strikethrough: true,
+ lax_spacing: true,
+ space_after_headers: true,
+ superscript: true,
+ footnotes: true
+ )
end
+
@markdown.render(text).html_safe
end
@@ -69,6 +72,7 @@ module GitlabMarkdownHelper
end
end
+ # TODO (rspeicher): This should be its own filter
def create_relative_links(text)
paths = extract_paths(text)
@@ -119,7 +123,7 @@ module GitlabMarkdownHelper
end
def ignored_protocols
- ["http://","https://", "ftp://", "mailto:"]
+ ["http://","https://", "ftp://", "mailto:", "smb://"]
end
def rebuild_path(file_path)
@@ -134,7 +138,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)
@@ -180,7 +184,7 @@ module GitlabMarkdownHelper
def file_exists?(path)
return false if path.nil?
- return @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any?
+ @repository.blob_at(current_sha, path).present? || @repository.tree(current_sha, path).entries.any?
end
# Check if the path is pointing to a directory(tree) or a file(blob)
@@ -188,7 +192,7 @@ module GitlabMarkdownHelper
def local_path(path)
return "tree" if @repository.tree(current_sha, path).entries.any?
return "raw" if @repository.blob_at(current_sha, path).image?
- return "blob"
+ "blob"
end
def current_sha
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 8518a47a3a0..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
@@ -45,7 +49,7 @@ module GitlabRoutingHelper
namespace_project_merge_request_url(entity.project.namespace, entity.project, entity, *args)
end
- def snippet_url(entity, *args)
+ def project_snippet_url(entity, *args)
namespace_project_snippet_url(entity.project.namespace, entity.project, entity, *args)
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 03fd461a462..3569ac2af63 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)
@@ -15,24 +19,6 @@ module GroupsHelper
end
end
- def group_head_title
- title = @group.name
-
- title = if current_action?(:issues)
- "Issues - " + title
- elsif current_action?(:merge_requests)
- "Merge requests - " + title
- elsif current_action?(:members)
- "Members - " + title
- elsif current_action?(:edit)
- "Settings - " + title
- else
- title
- end
-
- title
- end
-
def group_settings_page?
if current_controller?('groups')
current_action?('edit') || current_action?('projects')
@@ -40,4 +26,16 @@ module GroupsHelper
false
end
end
+
+ def group_icon(group)
+ if group.is_a?(String)
+ group = Group.find_by(path: group)
+ end
+
+ if group && group.avatar.present?
+ group.avatar.url
+ else
+ image_path('no_group_avatar.png')
+ end
+ end
end
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 15c5dcb6a25..36d3f371c1b 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -13,33 +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
- end
-
- def url_for_new_issue(project = @project)
- return '' if project.nil?
-
- project.issues_tracker.new_issue_url
+ if options[:only_path]
+ project.issues_tracker.project_path
+ else
+ project.issues_tracker.project_url
+ end
end
- def url_for_issue(issue_iid, project = @project)
+ def url_for_new_issue(project = @project, options = {})
return '' if project.nil?
- project.issues_tracker.issue_url(issue_iid)
+ if options[:only_path]
+ project.issues_tracker.new_issue_path
+ else
+ project.issues_tracker.new_issue_url
+ end
end
- def title_for_issue(issue_iid, project = @project)
+ def url_for_issue(issue_iid, project = @project, options = {})
return '' if project.nil?
- if project.default_issues_tracker?
- issue = project.issues.where(iid: issue_iid).first
- return issue.title if issue
+ if options[:only_path]
+ project.issues_tracker.issue_path(issue_iid)
+ else
+ project.issues_tracker.issue_url(issue_iid)
end
-
- ''
end
def issue_timestamp(issue)
@@ -58,22 +59,11 @@ module IssuesHelper
end
def bulk_update_milestone_options
- options_for_select(['None (backlog)']) +
+ options_for_select([['None (backlog)', -1]]) +
options_from_collection_for_select(project_active_milestones, 'id',
'title', params[:milestone_id])
end
- def bulk_update_assignee_options(project = @project)
- options_for_select(['None (unassigned)']) +
- options_from_collection_for_select(project.team.members, 'id',
- 'name', params[:assignee_id])
- end
-
- def assignee_options(object, project = @project)
- options_from_collection_for_select(project.team.members.sort_by(&:name),
- 'id', 'name', object.assignee_id)
- end
-
def milestone_options(object)
options_from_collection_for_select(object.project.milestones.active,
'id', 'title', object.milestone_id)
@@ -107,4 +97,7 @@ module IssuesHelper
xml.summary issue.title
end
end
+
+ # Required for Gitlab::Markdown::IssueReferenceFilter
+ module_function :url_for_issue
end
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 49063491abf..8272c177d59 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -1,4 +1,6 @@
module LabelsHelper
+ include ActionView::Helpers::TagHelper
+
def project_label_names
@project.labels.pluck(:title)
end
@@ -7,9 +9,13 @@ module LabelsHelper
label_color = label.color || Label::DEFAULT_COLOR
text_color = text_color_for_bg(label_color)
- content_tag :span, class: 'label color-label', style: "background-color:#{label_color};color:#{text_color}" do
- label.name
- end
+ # Intentionally not using content_tag here so that this method can be called
+ # by LabelReferenceFilter
+ span = %(<span class="label color-label") +
+ %( style="background-color: #{label_color}; color: #{text_color}">) +
+ escape_once(label.name) + '</span>'
+
+ span.html_safe
end
def suggested_colors
@@ -42,9 +48,16 @@ module LabelsHelper
r, g, b = bg_color.slice(1,7).scan(/.{2}/).map(&:hex)
if (r + g + b) > 500
- "#333"
+ '#333333'
else
- "#FFF"
+ '#FFFFFF'
end
end
+
+ def project_labels_options(project)
+ options_from_collection_for_select(project.labels, 'name', 'name', params[:label_name])
+ end
+
+ # Required for Gitlab::Markdown::LabelReferenceFilter
+ module_function :render_colored_label, :text_color_for_bg, :escape_once
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 3b1589da57f..45ee4fe4135 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -17,7 +17,7 @@ module MergeRequestsHelper
end
def new_mr_from_push_event(event, target_project)
- return {
+ {
merge_request: {
source_project_id: event.project.id,
target_project_id: target_project.id,
@@ -35,7 +35,7 @@ module MergeRequestsHelper
end
def ci_build_details_path(merge_request)
- merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha)
+ merge_request.source_project.ci_service.build_page(merge_request.last_commit.sha, merge_request.source_branch)
end
def merge_path_description(merge_request, separator)
@@ -49,4 +49,16 @@ module MergeRequestsHelper
def issues_sentence(issues)
issues.map { |i| "##{i.iid}" }.to_sentence
end
+
+ def mr_change_branches_path(merge_request)
+ new_namespace_project_merge_request_path(
+ @project.namespace, @project,
+ merge_request: {
+ source_project_id: @merge_request.source_project_id,
+ target_project_id: @merge_request.target_project_id,
+ source_branch: @merge_request.source_branch,
+ target_branch: nil
+ }
+ )
+ end
end
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index 59fdc0d49cc..93e33ebefd8 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -19,4 +19,16 @@ 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
+
+ grouped_milestones = Milestones::GroupService.new(milestones).execute
+ options_from_collection_for_select(grouped_milestones, 'title', 'title', params[:milestone_title])
+ end
end
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 2bcfde62830..b3132a1f3ba 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -28,7 +28,7 @@ module NamespacesHelper
def namespace_icon(namespace, size = 40)
if namespace.kind_of?(Group)
- group_icon(namespace.path)
+ group_icon(namespace)
else
avatar_icon(namespace.owner.email, size)
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 2b03269800e..9b1dd8b8e54 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -2,4 +2,20 @@ module NavHelper
def nav_menu_collapsed?
cookies[:collapsed_nav] == 'true'
end
+
+ def nav_sidebar_class
+ if nav_menu_collapsed?
+ "page-sidebar-collapsed"
+ else
+ "page-sidebar-expanded"
+ end
+ end
+
+ def nav_header_class
+ if nav_menu_collapsed?
+ "header-collapsed"
+ else
+ "header-expanded"
+ end
+ end
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index ab44fa6ee43..271b53aa2b6 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -9,6 +9,10 @@ module NotesHelper
hidden_field_tag(:target_id, note.noteable.id)
end
+ def note_editable?(note)
+ note.editable? && can?(current_user, :admin_note, note)
+ end
+
def link_to_commit_diff_line_note(note)
if note.for_commit_diff_line?
link_to(
diff --git a/app/helpers/oauth_helper.rb b/app/helpers/oauth_helper.rb
index 1a0ad17b607..997b91de077 100644
--- a/app/helpers/oauth_helper.rb
+++ b/app/helpers/oauth_helper.rb
@@ -20,6 +20,15 @@ module OauthHelper
def additional_providers
enabled_oauth_providers.reject{|provider| provider.to_s.starts_with?('ldap')}
end
-
+
+ def oauth_image_tag(provider, size = 64)
+ file_name = "#{provider.to_s.split('_').first}_#{size}.png"
+ image_tag(image_path("authbuttons/#{file_name}"), alt: "Sign in with #{provider.to_s.titleize}")
+ end
+
+ def oauth_active?(provider)
+ current_user.identities.exists?(provider: provider.to_s)
+ end
+
extend self
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
new file mode 100644
index 00000000000..01b6a63552c
--- /dev/null
+++ b/app/helpers/page_layout_helper.rb
@@ -0,0 +1,26 @@
+module PageLayoutHelper
+ def page_title(*titles)
+ @page_title ||= []
+
+ @page_title.push(*titles.compact) if titles.any?
+
+ @page_title.join(" | ")
+ end
+
+ def header_title(title = nil, title_url = nil)
+ if title
+ @header_title = title
+ @header_title_url = title_url
+ else
+ @header_title_url ? link_to(@header_title, @header_title_url) : @header_title
+ end
+ end
+
+ def sidebar(name = nil)
+ if name
+ @sidebar = name
+ else
+ @sidebar
+ end
+ end
+end
diff --git a/app/helpers/profile_helper.rb b/app/helpers/profile_helper.rb
index 9e37e44732a..780c7cd5133 100644
--- a/app/helpers/profile_helper.rb
+++ b/app/helpers/profile_helper.rb
@@ -1,10 +1,4 @@
module ProfileHelper
- def oauth_active_class(provider)
- if current_user.identities.exists?(provider: provider.to_s)
- 'active'
- end
- end
-
def show_profile_username_tab?
current_user.can_change_username?
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index a5d7372bbe5..96d2606f1a1 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
@@ -146,6 +160,10 @@ module ProjectsHelper
nav_tabs << feature if project.send :"#{feature}_enabled"
end
+ if project.issues_enabled || project.merge_requests_enabled
+ nav_tabs << [:milestones, :labels]
+ end
+
nav_tabs.flatten
end
@@ -174,46 +192,6 @@ module ProjectsHelper
'unknown'
end
- def project_head_title
- title = @project.name_with_namespace
-
- title = if current_controller?(:tree)
- "#{@project.path}\/#{@path} at #{@ref} - " + title
- elsif current_controller?(:issues)
- if current_action?(:show)
- "Issue ##{@issue.iid} - #{@issue.title} - " + title
- else
- "Issues - " + title
- end
- elsif current_controller?(:blob)
- if current_action?(:new) || current_action?(:create)
- "New file at #{@ref}"
- elsif current_action?(:show)
- "#{@blob.path} at #{@ref}"
- elsif @blob
- "Edit file #{@blob.path} at #{@ref}"
- end
- elsif current_controller?(:commits)
- "Commits at #{@ref} - " + title
- elsif current_controller?(:merge_requests)
- if current_action?(:show)
- "Merge request ##{@merge_request.iid} - " + title
- else
- "Merge requests - " + title
- end
- elsif current_controller?(:wikis)
- "Wiki - " + title
- elsif current_controller?(:network)
- "Network graph - " + title
- elsif current_controller?(:graphs)
- "Graphs - " + title
- else
- title
- end
-
- title
- end
-
def default_url_to_repo(project = nil)
project = project || @project
current_user ? project.url_to_repo : project.http_url_to_repo
@@ -232,12 +210,45 @@ module ProjectsHelper
end
def contribution_guide_url(project)
- if project && project.repository.contribution_guide
+ if project && contribution_guide = project.repository.contribution_guide
namespace_project_blob_path(
project.namespace,
project,
tree_join(project.default_branch,
- project.repository.contribution_guide.name)
+ contribution_guide.name)
+ )
+ end
+ end
+
+ def changelog_url(project)
+ if project && changelog = project.repository.changelog
+ namespace_project_blob_path(
+ project.namespace,
+ project,
+ tree_join(project.default_branch,
+ changelog.name)
+ )
+ end
+ end
+
+ def license_url(project)
+ if project && license = project.repository.license
+ namespace_project_blob_path(
+ project.namespace,
+ project,
+ tree_join(project.default_branch,
+ license.name)
+ )
+ end
+ end
+
+ def version_url(project)
+ if project && version = project.repository.version
+ namespace_project_blob_path(
+ project.namespace,
+ project,
+ tree_join(project.default_branch,
+ version.name)
)
end
end
@@ -265,4 +276,14 @@ module ProjectsHelper
"success"
end
end
+
+ def service_field_value(type, value)
+ return value unless type == 'password'
+
+ if value.present?
+ "***********"
+ else
+ nil
+ end
+ end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index cb829037697..c31a556ff7b 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -23,9 +23,9 @@ module SearchHelper
# Autocomplete results for various settings pages
def default_autocomplete
[
- { label: "My Profile settings", url: profile_path },
- { label: "My SSH Keys", url: profile_keys_path },
- { label: "My Dashboard", url: root_path },
+ { label: "Profile settings", url: profile_path },
+ { label: "SSH Keys", url: profile_keys_path },
+ { label: "Dashboard", url: root_path },
{ label: "Admin Section", url: admin_root_path },
]
end
@@ -60,7 +60,7 @@ module SearchHelper
{ label: "#{prefix} - Merge Requests", url: namespace_project_merge_requests_path(@project.namespace, @project) },
{ label: "#{prefix} - Milestones", url: namespace_project_milestones_path(@project.namespace, @project) },
{ label: "#{prefix} - Snippets", url: namespace_project_snippets_path(@project.namespace, @project) },
- { label: "#{prefix} - Team", url: namespace_project_team_index_path(@project.namespace, @project) },
+ { label: "#{prefix} - Members", url: namespace_project_project_members_path(@project.namespace, @project) },
{ label: "#{prefix} - Wiki", url: namespace_project_wikis_path(@project.namespace, @project) },
]
else
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 525266fb3b5..6def7793dc3 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -2,8 +2,8 @@ module SubmoduleHelper
include Gitlab::ShellAdapter
# links to files listing for submodule if submodule is a project on this server
- def submodule_links(submodule_item, ref = nil)
- url = @repository.submodule_url_for(ref, submodule_item.path)
+ def submodule_links(submodule_item, ref = nil, repository = @repository)
+ url = repository.submodule_url_for(ref, submodule_item.path)
return url, nil unless url =~ /([^\/:]+)\/([^\/]+\.git)\Z/
@@ -44,21 +44,31 @@ 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)
base = [ 'https://', host, '/', namespace, '/', project ].join('')
- return base, [ base, '/tree/', commit ].join('')
+ [base, [ base, '/tree/', commit ].join('')]
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.namespace.name
end
- return 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/tab_helper.rb b/app/helpers/tab_helper.rb
index 7a401a274d3..a1d263d9d3a 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -89,7 +89,7 @@ module TabHelper
def project_tab_class
return "active" if current_page?(controller: "/projects", action: :edit, id: @project)
- if ['services', 'hooks', 'deploy_keys', 'team_members', 'protected_branches'].include? controller.controller_name
+ if ['services', 'hooks', 'deploy_keys', 'project_members', 'protected_branches'].include? controller.controller_name
"active"
end
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index b6fb7a8aa5a..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)
@@ -56,7 +57,7 @@ module TreeHelper
ref ||= @ref
return false unless project.repository.branch_names.include?(ref)
- ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref)
+ ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
end
def tree_breadcrumbs(tree, max_links = 2)
diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb
index deb9c8b4d49..00d4c7f1051 100644
--- a/app/helpers/visibility_level_helper.rb
+++ b/app/helpers/visibility_level_helper.rb
@@ -10,7 +10,21 @@ module VisibilityLevelHelper
end
end
- def visibility_level_description(level)
+ # Return the description for the +level+ argument.
+ #
+ # +level+ One of the Gitlab::VisibilityLevel constants
+ # +form_model+ Either a model object (Project, Snippet, etc.) or the name of
+ # a Project or Snippet class.
+ def visibility_level_description(level, form_model)
+ case form_model.is_a?(String) ? form_model : form_model.class.name
+ when 'PersonalSnippet', 'ProjectSnippet', 'Snippet'
+ snippet_visibility_level_description(level)
+ when 'Project'
+ project_visibility_level_description(level)
+ end
+ end
+
+ def project_visibility_level_description(level)
capture_haml do
haml_tag :span do
case level
@@ -33,7 +47,7 @@ module VisibilityLevelHelper
haml_tag :span do
case level
when Gitlab::VisibilityLevel::PRIVATE
- haml_concat "The snippet is visible only for me"
+ haml_concat "The snippet is visible only for me."
when Gitlab::VisibilityLevel::INTERNAL
haml_concat "The snippet is visible for any logged in user."
when Gitlab::VisibilityLevel::PUBLIC
@@ -60,7 +74,16 @@ module VisibilityLevelHelper
Project.visibility_levels.key(level)
end
- def restricted_visibility_levels
- current_user.is_admin? ? [] : gitlab_config.restricted_visibility_levels
+ def restricted_visibility_levels(show_all = false)
+ return [] if current_user.is_admin? && !show_all
+ current_application_settings.restricted_visibility_levels || []
+ end
+
+ def default_project_visibility
+ current_application_settings.default_project_visibility
+ end
+
+ def default_snippet_visibility
+ current_application_settings.default_snippet_visibility
end
end
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
new file mode 100644
index 00000000000..f8a96516e61
--- /dev/null
+++ b/app/helpers/wiki_helper.rb
@@ -0,0 +1,24 @@
+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
+ when String
+ 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/devise_mailer.rb b/app/mailers/devise_mailer.rb
new file mode 100644
index 00000000000..b616add283a
--- /dev/null
+++ b/app/mailers/devise_mailer.rb
@@ -0,0 +1,4 @@
+class DeviseMailer < Devise::Mailer
+ default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>"
+ default reply_to: Gitlab.config.gitlab.email_reply_to
+end
diff --git a/app/mailers/emails/groups.rb b/app/mailers/emails/groups.rb
index 8c09389985e..1c43f95dc8c 100644
--- a/app/mailers/emails/groups.rb
+++ b/app/mailers/emails/groups.rb
@@ -1,11 +1,52 @@
module Emails
module Groups
- def group_access_granted_email(user_group_id)
- @membership = GroupMember.find(user_group_id)
- @group = @membership.group
+ 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: @membership.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 9ea121d83a4..9cb7077e59d 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,53 +1,141 @@
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,
subject: subject("Project was moved"))
end
- def repository_push_email(project_id, recipient, author_id, branch, compare, reverse_compare = false, send_from_committer_email = false, disable_diffs = false)
+ def repository_push_email(project_id, recipient, author_id: nil,
+ ref: nil,
+ action: nil,
+ compare: nil,
+ reverse_compare: false,
+ send_from_committer_email: false,
+ disable_diffs: false)
+ unless author_id && ref && action
+ raise ArgumentError, "missing keywords: author_id, ref, action"
+ end
+
@project = Project.find(project_id)
- @author = User.find(author_id)
+ @current_user = @author = User.find(author_id)
@reverse_compare = reverse_compare
@compare = compare
- @commits = Commit.decorate(compare.commits)
- @diffs = compare.diffs
- @branch = branch.gsub("refs/heads/", "")
+ @ref_name = Gitlab::Git.ref_name(ref)
+ @ref_type = Gitlab::Git.tag_ref?(ref) ? "tag" : "branch"
+ @action = action
@disable_diffs = disable_diffs
- @subject = "[#{@project.path_with_namespace}][#{@branch}] "
+ if @compare
+ @commits = Commit.decorate(compare.commits, @project)
+ @diffs = compare.diffs
+ end
+
+ @action_name =
+ case action
+ when :create
+ "pushed new"
+ when :delete
+ "deleted"
+ else
+ "pushed to"
+ end
- if @commits.length > 1
- @target_url = namespace_project_compare_url(@project.namespace,
- @project,
- from: Commit.new(@compare.base),
- to: Commit.new(@compare.head))
- @subject << "Deleted " if @reverse_compare
- @subject << "#{@commits.length} commits: #{@commits.first.title}"
+ @subject = "[#{@project.path_with_namespace}]"
+ @subject << "[#{@ref_name}]" if action == :push
+ @subject << " "
+
+ if action == :push
+ if @commits.length > 1
+ @target_url = namespace_project_compare_url(@project.namespace,
+ @project,
+ from: Commit.new(@compare.base, @project),
+ to: Commit.new(@compare.head, @project))
+ @subject << "Deleted " if @reverse_compare
+ @subject << "#{@commits.length} commits: #{@commits.first.title}"
+ else
+ @target_url = namespace_project_commit_url(@project.namespace,
+ @project, @commits.first)
+
+ @subject << "Deleted 1 commit: " if @reverse_compare
+ @subject << @commits.first.title
+ end
else
- @target_url = namespace_project_commit_url(@project.namespace,
- @project, @commits.first)
+ unless action == :delete
+ @target_url = namespace_project_tree_url(@project.namespace,
+ @project, @ref_name)
+ end
- @subject << "Deleted 1 commit: " if @reverse_compare
- @subject << @commits.first.title
+ subject_action = @action_name.dup
+ subject_action[0] = subject_action[0].capitalize
+ @subject << "#{subject_action} #{@ref_type} #{@ref_name}"
end
@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 65925b61e94..79fb48b00d3 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -13,13 +13,11 @@ class Notify < ActionMailer::Base
add_template_helper MergeRequestsHelper
add_template_helper EmailsHelper
- 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
+ attr_accessor :current_user
+ helper_method :current_user, :can?
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
@@ -53,24 +51,28 @@ class Notify < ActionMailer::Base
# The default email address to send emails from
def default_sender_address
address = Mail::Address.new(Gitlab.config.gitlab.email_from)
- address.display_name = "GitLab"
+ address.display_name = Gitlab.config.gitlab.email_display_name
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
+ return unless sender = User.find(sender_id)
- sender_domain = sender.email.split("@").last
- if send_from_user_email && self.class.allowed_email_domains.include?(sender_domain)
- address.address = sender.email
- end
+ 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 +81,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
@@ -148,10 +149,14 @@ class Notify < ActionMailer::Base
headers['References'] = message_id(model)
headers['X-GitLab-Project'] = "#{@project.name} | " if @project
- if (headers[:subject])
+ if headers[:subject]
headers[:subject].prepend('Re: ')
end
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 890417e780d..85a15596f8d 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -14,7 +14,7 @@ class Ability
when "MergeRequest" then merge_request_abilities(user, subject)
when "Group" then group_abilities(user, subject)
when "Namespace" then namespace_abilities(user, subject)
- when "GroupMember" then users_group_abilities(user, subject)
+ when "GroupMember" then group_member_abilities(user, subject)
else []
end.concat(global_abilities(user))
end
@@ -37,7 +37,7 @@ class Ability
:read_issue,
:read_milestone,
:read_project_snippet,
- :read_team_member,
+ :read_project_member,
:read_merge_request,
:read_note,
:download_code
@@ -119,7 +119,7 @@ class Ability
:read_issue,
:read_milestone,
:read_project_snippet,
- :read_team_member,
+ :read_project_member,
:read_merge_request,
:read_note,
:write_project,
@@ -166,7 +166,7 @@ class Ability
:admin_issue,
:admin_milestone,
:admin_project_snippet,
- :admin_team_member,
+ :admin_project_member,
:admin_merge_request,
:admin_note,
:admin_wiki,
@@ -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
@@ -225,13 +225,15 @@ class Ability
[:issue, :note, :project_snippet, :personal_snippet, :merge_request].each do |name|
define_method "#{name}_abilities" do |user, subject|
- if subject.author == user
- [
+ if subject.author == user || user.is_admin?
+ rules = [
:"read_#{name}",
:"write_#{name}",
:"modify_#{name}",
:"admin_#{name}"
]
+ rules.push(:change_visibility_level) if subject.is_a?(Snippet)
+ rules
elsif subject.respond_to?(:assignee) && subject.assignee == user
[
:"read_#{name}",
@@ -248,17 +250,17 @@ class Ability
end
end
- def users_group_abilities(user, subject)
+ def group_member_abilities(user, subject)
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
- rules << :destroy
+ rules << :modify_group_member
+ rules << :destroy_group_member
end
if !group.last_owner?(user) && (can_manage || (user == target_user))
- rules << :destroy
+ rules << :destroy_group_member
end
rules
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 588668b3d1e..d5123249c53 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -2,25 +2,44 @@
#
# Table name: application_settings
#
-# id :integer not null, primary key
-# default_projects_limit :integer
-# signup_enabled :boolean
-# signin_enabled :boolean
-# gravatar_enabled :boolean
-# sign_in_text :text
-# created_at :datetime
-# updated_at :datetime
-# home_page_url :string(255)
-# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
+# id :integer not null, primary key
+# default_projects_limit :integer
+# signup_enabled :boolean
+# signin_enabled :boolean
+# gravatar_enabled :boolean
+# sign_in_text :text
+# created_at :datetime
+# updated_at :datetime
+# home_page_url :string(255)
+# default_branch_protection :integer default(2)
+# twitter_sharing_enabled :boolean default(TRUE)
+# restricted_visibility_levels :text
+# max_attachment_size :integer default(10), not null
+# default_project_visibility :integer
+# default_snippet_visibility :integer
+# restricted_signup_domains :text
#
class ApplicationSetting < ActiveRecord::Base
+ serialize :restricted_visibility_levels
+ serialize :restricted_signup_domains, Array
+ attr_accessor :restricted_signup_domains_raw
+
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|
+ unless value.nil?
+ value.each do |level|
+ unless Gitlab::VisibilityLevel.options.has_value?(level)
+ record.errors.add(attr, "'#{level}' is not a valid visibility level")
+ end
+ end
+ end
+ end
+
def self.current
ApplicationSetting.last
end
@@ -34,10 +53,32 @@ 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'],
+ max_attachment_size: Settings.gitlab['max_attachment_size'],
+ default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
+ restricted_signup_domains: Settings.gitlab['restricted_signup_domains']
)
end
def home_page_url_column_exist
ActiveRecord::Base.connection.column_exists?(:application_settings, :home_page_url)
end
+
+ def restricted_signup_domains_raw
+ self.restricted_signup_domains.join("\n") unless self.restricted_signup_domains.nil?
+ end
+
+ def restricted_signup_domains_raw=(values)
+ self.restricted_signup_domains = []
+ self.restricted_signup_domains = values.split(
+ /\s*[,;]\s* # comma or semicolon, optionally surrounded by whitespace
+ | # or
+ \s # any whitespace character
+ | # or
+ [\r\n] # any number of newline characters
+ /x)
+ self.restricted_signup_domains.reject! { |d| d.empty? }
+ end
+
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index e0461809e10..be5a118bfec 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -3,8 +3,12 @@ class Commit
include StaticModel
extend ActiveModel::Naming
include Mentionable
+ include Participable
attr_mentionable :safe_message
+ participant :author, :committer, :notes, :mentioned_users
+
+ attr_accessor :project
# Safe amount of changes (files and lines) in one commit to render
# Used to prevent 500 error on huge commits by suppressing diff
@@ -18,12 +22,12 @@ class Commit
DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES)
class << self
- def decorate(commits)
+ def decorate(commits, project)
commits.map do |commit|
if commit.kind_of?(Commit)
commit
else
- self.new(commit)
+ self.new(commit, project)
end
end
end
@@ -41,10 +45,11 @@ class Commit
attr_accessor :raw
- def initialize(raw_commit)
+ def initialize(raw_commit, project)
raise "Nil as raw commit passed" unless raw_commit
@raw = raw_commit
+ @project = project
end
def id
@@ -77,7 +82,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 +95,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
@@ -100,7 +105,7 @@ class Commit
description.present?
end
- def hook_attrs(project)
+ def hook_attrs
path_with_namespace = project.path_with_namespace
{
@@ -117,8 +122,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(current_user = self.committer)
+ Gitlab::ClosingIssueExtractor.new(project, current_user).closed_by_message(safe_message)
end
# Mentionable override.
@@ -126,6 +131,18 @@ 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 notes
+ project.notes.for_commit_id(self.id)
+ end
+
def method_missing(m, *args, &block)
@raw.send(m, *args, &block)
end
@@ -142,6 +159,6 @@ class Commit
end
def parents
- @parents ||= Commit.decorate(super)
+ @parents ||= Commit.decorate(super, project)
end
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
new file mode 100644
index 00000000000..e6456198264
--- /dev/null
+++ b/app/models/commit_range.rb
@@ -0,0 +1,106 @@
+# CommitRange makes it easier to work with commit ranges
+#
+# Examples:
+#
+# range = CommitRange.new('f3f85602...e86e1013')
+# range.exclude_start? # => false
+# range.reference_title # => "Commits f3f85602 through e86e1013"
+# range.to_s # => "f3f85602...e86e1013"
+#
+# range = CommitRange.new('f3f856029bc5f966c5a7ee24cf7efefdd20e6019..e86e1013709735be5bb767e2b228930c543f25ae')
+# range.exclude_start? # => true
+# range.reference_title # => "Commits f3f85602^ through e86e1013"
+# range.to_param # => {from: "f3f856029bc5f966c5a7ee24cf7efefdd20e6019^", to: "e86e1013709735be5bb767e2b228930c543f25ae"}
+# range.to_s # => "f3f85602..e86e1013"
+#
+# # Assuming `project` is a Project with a repository containing both commits:
+# range.project = project
+# range.valid_commits? # => true
+#
+class CommitRange
+ include ActiveModel::Conversion
+
+ attr_reader :sha_from, :notation, :sha_to
+
+ # Optional Project model
+ attr_accessor :project
+
+ # See `exclude_start?`
+ attr_reader :exclude_start
+
+ # The beginning and ending SHA sums can be between 6 and 40 hex characters,
+ # and the range selection can be double- or triple-dot.
+ PATTERN = /\h{6,40}\.{2,3}\h{6,40}/
+
+ # Initialize a CommitRange
+ #
+ # range_string - The String commit range.
+ # project - An optional Project model.
+ #
+ # Raises ArgumentError if `range_string` does not match `PATTERN`.
+ def initialize(range_string, project = nil)
+ range_string.strip!
+
+ unless range_string.match(/\A#{PATTERN}\z/)
+ raise ArgumentError, "invalid CommitRange string format: #{range_string}"
+ end
+
+ @exclude_start = !range_string.include?('...')
+ @sha_from, @notation, @sha_to = range_string.split(/(\.{2,3})/, 2)
+
+ @project = project
+ end
+
+ def inspect
+ %(#<#{self.class}:#{object_id} #{to_s}>)
+ end
+
+ def to_s
+ "#{sha_from[0..7]}#{notation}#{sha_to[0..7]}"
+ end
+
+ # Returns a String for use in a link's title attribute
+ def reference_title
+ "Commits #{suffixed_sha_from} through #{sha_to}"
+ end
+
+ # Return a Hash of parameters for passing to a URL helper
+ #
+ # See `namespace_project_compare_url`
+ def to_param
+ { from: suffixed_sha_from, to: sha_to }
+ end
+
+ def exclude_start?
+ exclude_start
+ end
+
+ # Check if both the starting and ending commit IDs exist in a project's
+ # repository
+ #
+ # project - An optional Project to check (default: `project`)
+ def valid_commits?(project = project)
+ return nil unless project.present?
+ return false unless project.valid_repo?
+
+ commit_from.present? && commit_to.present?
+ end
+
+ def persisted?
+ true
+ end
+
+ def commit_from
+ @commit_from ||= project.repository.commit(suffixed_sha_from)
+ end
+
+ def commit_to
+ @commit_to ||= project.repository.commit(sha_to)
+ end
+
+ private
+
+ def suffixed_sha_from
+ sha_from + (exclude_start? ? '^' : '')
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index f5e23e9dc2d..97846b06d72 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -7,6 +7,7 @@
module Issuable
extend ActiveSupport::Concern
include Mentionable
+ include Participable
included do
belongs_to :author, class_name: "User"
@@ -15,6 +16,7 @@ module Issuable
has_many :notes, as: :noteable, dependent: :destroy
has_many :label_links, as: :target, dependent: :destroy
has_many :labels, through: :label_links
+ has_many :subscriptions, dependent: :destroy, as: :subscribable
validates :author, presence: true
validates :title, presence: true, length: { within: 0..255 }
@@ -44,6 +46,7 @@ module Issuable
prefix: true
attr_mentionable :title, :description
+ participant :author, :assignee, :notes, :mentioned_users
end
module ClassMethods
@@ -116,20 +119,20 @@ module Issuable
upvotes + downvotes
end
- # Return all users participating on the discussion
- def participants
- users = []
- users << author
- users << assignee if is_assigned?
- mentions = []
- mentions << self.mentioned_users
+ def subscribed?(user)
+ subscription = subscriptions.find_by_user_id(user.id)
- notes.each do |note|
- users << note.author
- mentions << note.mentioned_users
+ if subscription
+ return subscription.subscribed
end
- users.concat(mentions.reduce([], :|)).uniq
+ participants(user).include?(user)
+ end
+
+ def toggle_subscription(user)
+ subscriptions.
+ find_or_initialize_by(user_id: user.id).
+ update(subscribed: !subscribed?(user))
end
def to_hook_data(user)
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index 74900d4675d..3ef3e8b67d8 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -28,7 +28,7 @@ module Mentionable
# Construct a String that contains possible GFM references.
def mentionable_text
- self.class.mentionable_attrs.map { |attr| send(attr) || '' }.join
+ self.class.mentionable_attrs.map { |attr| send(attr) }.compact.join("\n\n")
end
# The GFM reference to this Mentionable, which shouldn't be included in its #references.
@@ -42,42 +42,29 @@ 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, p = project)
+ return [] if mentionable_text.blank?
+
+ ext = Gitlab::ReferenceExtractor.new(p, 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+.
def create_cross_references!(p = project, a = author, without = [])
refs = references(p) - without
refs.each do |ref|
- Note.create_cross_reference_note(ref, local_reference, a, p)
+ Note.create_cross_reference_note(ref, local_reference, a)
end
end
@@ -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/concerns/participable.rb b/app/models/concerns/participable.rb
new file mode 100644
index 00000000000..7a5e4876ff2
--- /dev/null
+++ b/app/models/concerns/participable.rb
@@ -0,0 +1,65 @@
+# == Participable concern
+#
+# Contains functionality related to objects that can have participants, such as
+# an author, an assignee and people mentioned in its description or comments.
+#
+# Used by Issue, Note, MergeRequest, Snippet and Commit.
+#
+# Usage:
+#
+# class Issue < ActiveRecord::Base
+# include Participable
+#
+# # ...
+#
+# participant :author, :assignee, :mentioned_users, :notes
+# end
+#
+# issue = Issue.last
+# users = issue.participants
+# # `users` will contain the issue's author, its assignee,
+# # all users returned by its #mentioned_users method,
+# # as well as all participants to all of the issue's notes,
+# # since Note implements Participable as well.
+#
+module Participable
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def participant(*attrs)
+ participant_attrs.concat(attrs.map(&:to_s))
+ end
+
+ def participant_attrs
+ @participant_attrs ||= []
+ end
+ end
+
+ def participants(current_user = self.author)
+ self.class.participant_attrs.flat_map do |attr|
+ meth = method(attr)
+
+ value =
+ if meth.arity == 1
+ meth.call(current_user)
+ else
+ meth.call
+ end
+
+ participants_for(value, current_user)
+ end.compact.uniq
+ end
+
+ private
+
+ def participants_for(value, current_user = nil)
+ case value
+ when User
+ [value]
+ when Enumerable, ActiveRecord::Relation
+ value.flat_map { |v| participants_for(v, current_user) }
+ when Participable
+ value.participants(current_user)
+ end
+ end
+end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index 410e8dc820b..33b4814d7ec 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -1,51 +1,36 @@
+require 'task_list'
+
# Contains functionality for objects that can have task lists in their
# descriptions. Task list items can be added with Markdown like "* [x] Fix
# bugs".
#
# Used by MergeRequest and Issue
module Taskable
- TASK_PATTERN_MD = /^(?<bullet> *[*-] *)\[(?<checked>[ xX])\]/.freeze
- TASK_PATTERN_HTML = /^<li>\[(?<checked>[ xX])\]/.freeze
-
- # Change the state of a task list item for this Taskable. Edit the object's
- # description by finding the nth task item and changing its checkbox
- # placeholder to "[x]" if +checked+ is true, or "[ ]" if it's false.
- # Note: task numbering starts with 1
- def update_nth_task(n, checked)
- index = 0
- check_char = checked ? 'x' : ' '
+ # Called by `TaskList::Summary`
+ def task_list_items
+ return [] if description.blank?
- # Do this instead of using #gsub! so that ActiveRecord detects that a field
- # has changed.
- self.description = self.description.gsub(TASK_PATTERN_MD) do |match|
- index += 1
- case index
- when n then "#{$LAST_MATCH_INFO[:bullet]}[#{check_char}]"
- else match
- end
+ @task_list_items ||= description.scan(TaskList::Filter::ItemPattern).collect do |item|
+ # ItemPattern strips out the hyphen, but Item requires it. Rabble rabble.
+ TaskList::Item.new("- #{item}")
end
+ end
- save
+ def tasks
+ @tasks ||= TaskList.new(self)
end
# Return true if this object's description has any task list items.
def tasks?
- description && description.match(TASK_PATTERN_MD)
+ tasks.summary.items?
end
# Return a string that describes the current state of this Taskable's task
- # list items, e.g. "20 tasks (12 done, 8 unfinished)"
+ # list items, e.g. "20 tasks (12 completed, 8 remaining)"
def task_status
- return nil unless description
-
- num_tasks = 0
- num_done = 0
-
- description.scan(TASK_PATTERN_MD) do
- num_tasks += 1
- num_done += 1 unless $LAST_MATCH_INFO[:checked] == ' '
- end
+ return '' if description.blank?
- "#{num_tasks} tasks (#{num_done} done, #{num_tasks - num_done} unfinished)"
+ sum = tasks.summary
+ "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
end
end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index 570f5e91c13..9ab663c04ad 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -10,6 +10,7 @@
# title :string(255)
# type :string(255)
# fingerprint :string(255)
+# public :boolean default(FALSE), not null
#
class DeployKey < Key
@@ -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 f23d8205ddc..18db521741f 100644
--- a/app/models/deploy_keys_project.rb
+++ b/app/models/deploy_keys_project.rb
@@ -16,4 +16,14 @@ class DeployKeysProject < ActiveRecord::Base
validates :deploy_key_id, presence: true
validates :deploy_key_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
validates :project_id, presence: true
+
+ after_destroy :destroy_orphaned_deploy_key
+
+ private
+
+ def destroy_orphaned_deploy_key
+ return unless self.deploy_key.destroyed_when_orphaned? && self.deploy_key.orphaned?
+
+ self.deploy_key.destroy
+ end
end
diff --git a/app/models/email.rb b/app/models/email.rb
index 556b0e9586e..935705e2ed4 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -18,7 +18,6 @@ class Email < ActiveRecord::Base
validates :email, presence: true, email: { strict_mode: true }, uniqueness: true
validate :unique_email, if: ->(email) { email.email_changed? }
- after_create :notify
before_validation :cleanup_email
def cleanup_email
@@ -28,8 +27,4 @@ class Email < ActiveRecord::Base
def unique_email
self.errors.add(:email, 'has already been taken') if User.exists?(email: self.email)
end
-
- def notify
- NotificationService.new.new_email(self)
- end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 5579ab1dbb0..c9a88ffa8e0 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -55,6 +55,12 @@ class Event < ActiveRecord::Base
order('id DESC').limit(100).
update_all(updated_at: Time.now)
end
+
+ def contributions
+ where("action = ? OR (target_type in (?) AND action in (?))",
+ Event::PUSHED, ["MergeRequest", "Issue"],
+ [Event::CREATED, Event::CLOSED, Event::MERGED])
+ end
end
def proper?
@@ -177,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
@@ -190,19 +200,19 @@ class Event < ActiveRecord::Base
end
def tag?
- data[:ref]["refs/tags"]
+ Gitlab::Git.tag_ref?(data[:ref])
end
def branch?
- data[:ref]["refs/heads"]
+ Gitlab::Git.branch_ref?(data[:ref])
end
def new_ref?
- commit_from =~ /^00000/
+ Gitlab::Git.blank_ref?(commit_from)
end
def rm_ref?
- commit_to =~ /^00000/
+ Gitlab::Git.blank_ref?(commit_to)
end
def md_ref?
@@ -226,11 +236,11 @@ class Event < ActiveRecord::Base
end
def branch_name
- @branch_name ||= data[:ref].gsub("refs/heads/", "")
+ @branch_name ||= Gitlab::Git.ref_name(data[:ref])
end
def tag_name
- @tag_name ||= data[:ref].gsub("refs/tags/", "")
+ @tag_name ||= Gitlab::Git.ref_name(data[:ref])
end
# Max 20 commits from push DESC
@@ -247,7 +257,7 @@ class Event < ActiveRecord::Base
end
def push_with_commits?
- md_ref? && commits.any? && commit_from && commit_to
+ !commits.empty? && commit_from && commit_to
end
def last_push_to_non_root?
diff --git a/app/models/external_issue.rb b/app/models/external_issue.rb
index 50efcb32f1b..85fdb12bfdc 100644
--- a/app/models/external_issue.rb
+++ b/app/models/external_issue.rb
@@ -15,6 +15,10 @@ class ExternalIssue
@issue_identifier.to_s
end
+ def title
+ "External Issue #{self}"
+ end
+
def ==(other)
other.is_a?(self.class) && (to_s == other.to_s)
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/issue.rb b/app/models/issue.rb
index 19e43ebd788..6e102051387 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -32,7 +32,6 @@ class Issue < ActiveRecord::Base
validates :project, presence: true
scope :of_group, ->(group) { where(project_id: group.project_ids) }
- scope :of_user_team, ->(team) { where(project_id: team.project_ids, assignee_id: team.member_ids) }
scope :cared, ->(user) { where(assignee_id: user) }
scope :open_for, ->(user) { opened.assigned_to(user) }
diff --git a/app/models/key.rb b/app/models/key.rb
index e2e59296eed..bbc28678177 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -10,13 +10,13 @@
# title :string(255)
# type :string(255)
# fingerprint :string(255)
+# public :boolean default(FALSE), not null
#
require 'digest/md5'
class Key < ActiveRecord::Base
include Sortable
- include Gitlab::Popen
belongs_to :user
@@ -79,20 +79,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/label.rb b/app/models/label.rb
index 9d7099c5652..eee28acefc1 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -13,6 +13,8 @@
class Label < ActiveRecord::Base
DEFAULT_COLOR = '#428BCA'
+ default_value_for :color, DEFAULT_COLOR
+
belongs_to :project
has_many :label_links, dependent: :destroy
has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
@@ -25,7 +27,7 @@ class Label < ActiveRecord::Base
# Don't allow '?', '&', and ',' for label titles
validates :title,
presence: true,
- format: { with: /\A[^&\?,&]+\z/ },
+ format: { with: /\A[^&\?,]+\z/ },
uniqueness: { scope: :project_id }
default_scope { order(title: :asc) }
diff --git a/app/models/member.rb b/app/models/member.rb
index fe3d2f40e87..cae8caa23fb 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -6,11 +6,15 @@
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
-# user_id :integer not null
+# user_id :integer
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string(255)
+# invite_token :string(255)
+# 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..65d2ea00570 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -6,11 +6,15 @@
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
-# user_id :integer not null
+# user_id :integer
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string(255)
+# invite_token :string(255)
+# invite_accepted_at :datetime
#
class GroupMember < Member
@@ -27,10 +31,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 +43,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 e4791d0f0aa..1b0c76917aa 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -6,11 +6,15 @@
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
-# user_id :integer not null
+# user_id :integer
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string(255)
+# invite_token :string(255)
+# invite_accepted_at :datetime
#
class ProjectMember < Member
@@ -27,10 +31,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 +55,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 +64,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 +84,7 @@ class ProjectMember < Member
def truncate_teams(project_ids)
ProjectMember.transaction do
members = ProjectMember.where(source_id: project_ids)
+
members.each do |member|
member.destroy
end
@@ -109,41 +112,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_team_member(self)
+ notification_service.new_project_member(self)
end
- system_hook_service.execute_hooks_for(self, :create)
+ super
end
def post_update_hook
- notification_service.update_team_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 f758126cfeb..64f3c39f131 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -95,16 +95,25 @@ class MergeRequest < ActiveRecord::Base
end
event :mark_as_mergeable do
- transition unchecked: :can_be_merged
+ transition [:unchecked, :cannot_be_merged] => :can_be_merged
end
event :mark_as_unmergeable do
- transition unchecked: :cannot_be_merged
+ transition [:unchecked, :can_be_merged] => :cannot_be_merged
end
state :unchecked
state :can_be_merged
state :cannot_be_merged
+
+ around_transition do |merge_request, transition, block|
+ merge_request.record_timestamps = false
+ begin
+ block.call
+ ensure
+ merge_request.record_timestamps = true
+ end
+ end
end
validates :source_project, presence: true, unless: :allow_broken
@@ -115,7 +124,6 @@ class MergeRequest < ActiveRecord::Base
validate :validate_fork
scope :of_group, ->(group) { where("source_project_id in (:group_project_ids) OR target_project_id in (:group_project_ids)", group_project_ids: group.project_ids) }
- scope :of_user_team, ->(team) { where("(source_project_id in (:team_project_ids) OR target_project_id in (:team_project_ids) AND assignee_id in (:team_member_ids))", team_project_ids: team.project_ids, team_member_ids: team.member_ids) }
scope :merged, -> { with_state(:merged) }
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
@@ -191,6 +199,8 @@ class MergeRequest < ActiveRecord::Base
end
def automerge!(current_user, commit_message = nil)
+ return unless automergeable?
+
MergeRequests::AutoMergeService.
new(target_project, current_user).
execute(self, commit_message)
@@ -200,15 +210,34 @@ class MergeRequest < ActiveRecord::Base
opened? || reopened?
end
+ def work_in_progress?
+ title =~ /\A\[?WIP\]?:? /i
+ end
+
+ def automergeable?
+ open? && !work_in_progress? && can_be_merged?
+ end
+
+ def automerge_status
+ if work_in_progress?
+ "work_in_progress"
+ else
+ merge_status_name
+ end
+ end
+
def mr_and_commit_notes
# Fetch comments only from last 100 commits
commits_for_notes_limit = 100
commit_ids = commits.last(commits_for_notes_limit).map(&:id)
- project.notes.where(
- "(noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR (noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
+ Note.where(
+ "(project_id = :target_project_id AND noteable_type = 'MergeRequest' AND noteable_id = :mr_id) OR" +
+ "(project_id = :source_project_id AND noteable_type = 'Commit' AND commit_id IN (:commit_ids))",
mr_id: id,
- commit_ids: commit_ids
+ commit_ids: commit_ids,
+ target_project_id: target_project_id,
+ source_project_id: source_project_id
)
end
@@ -234,7 +263,7 @@ class MergeRequest < ActiveRecord::Base
}
unless last_commit.nil?
- attrs.merge!(last_commit: last_commit.hook_attrs(source_project))
+ attrs.merge!(last_commit: last_commit.hook_attrs)
end
attributes.merge!(attrs)
@@ -249,11 +278,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(current_user) }
+ issues.push(*Gitlab::ClosingIssueExtractor.new(project, current_user).
+ closed_by_message(description))
issues.uniq.sort_by(&:id)
else
[]
@@ -353,6 +382,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/merge_request_diff.rb b/app/models/merge_request_diff.rb
index acac1ca4cf7..df1c2b78758 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -67,7 +67,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_commits(array)
- array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash)) }
+ array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
end
def dump_diffs(diffs)
@@ -88,7 +88,7 @@ class MergeRequestDiff < ActiveRecord::Base
commits = compare_result.commits
if commits.present?
- commits = Commit.decorate(commits).
+ commits = Commit.decorate(commits, merge_request.source_project).
sort_by(&:created_at).
reverse
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 35280889a86..211dfa76b81 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,46 @@ 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
+ # Get the email username by removing everything after an `@` sign.
+ path.gsub!(/@.*\z/, "")
+ # Usernames can't end in .git, so remove it.
+ path.gsub!(/\.git\z/, "")
+ # Remove dashes at the start of the username.
+ path.gsub!(/\A-+/, "")
+ # Remove periods at the end of the username.
+ path.gsub!(/\.+\z/, "")
+ # Remove everything that's not in the list of allowed characters.
+ path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+
+ # Users with the great usernames of "." or ".." would end up with a blank username.
+ # Work around that by setting their username to "blank", followed by a counter.
+ path = "blank" if path.blank?
+
+ counter = 0
+ base = path
+ while Namespace.find_by_path_or_name(path)
+ 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 9ca3e4d7e97..cbce6786683 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -22,10 +22,13 @@ require 'file_size_validator'
class Note < ActiveRecord::Base
include Mentionable
+ include Gitlab::CurrentSettings
+ include Participable
default_value_for :system, false
attr_mentionable :note
+ participant :author, :mentioned_users
belongs_to :project
belongs_to :noteable, polymorphic: true
@@ -36,7 +39,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' }
@@ -48,6 +52,7 @@ class Note < ActiveRecord::Base
scope :inline, ->{ where("line_code IS NOT NULL") }
scope :not_inline, ->{ where(line_code: [nil, '']) }
scope :system, ->{ where(system: true) }
+ scope :user, ->{ where(system: false) }
scope :common, ->{ where(noteable_type: ["", nil]) }
scope :fresh, ->{ order(created_at: :asc, id: :asc) }
scope :inc_author_project, ->{ includes(:project, :author) }
@@ -59,7 +64,7 @@ class Note < ActiveRecord::Base
class << self
def create_status_change_note(noteable, project, author, status, source)
- body = "_Status changed to #{status}#{' by ' + source.gfm_reference if source}_"
+ body = "Status changed to #{status}#{' by ' + source.gfm_reference if source}"
create(
noteable: noteable,
@@ -74,11 +79,11 @@ class Note < ActiveRecord::Base
# +mentioner+'s description or an associated Note.
# Create a system Note associated with +noteable+ with a GFM back-reference
# to +mentioner+.
- def create_cross_reference_note(noteable, mentioner, author, project)
- gfm_reference = mentioner_gfm_ref(noteable, mentioner, project)
+ def create_cross_reference_note(noteable, mentioner, author)
+ gfm_reference = mentioner_gfm_ref(noteable, mentioner)
note_options = {
- project: project,
+ project: noteable.project,
author: author,
note: cross_reference_note_content(gfm_reference),
system: true
@@ -95,9 +100,9 @@ class Note < ActiveRecord::Base
def create_milestone_change_note(noteable, project, author, milestone)
body = if milestone.nil?
- '_Milestone removed_'
+ 'Milestone removed'
else
- "_Milestone changed to #{milestone.title}_"
+ "Milestone changed to #{milestone.title}"
end
create(
@@ -110,7 +115,7 @@ class Note < ActiveRecord::Base
end
def create_assignee_change_note(noteable, project, author, assignee)
- body = assignee.nil? ? '_Assignee removed_' : "_Reassigned to @#{assignee.username}_"
+ body = assignee.nil? ? 'Assignee removed' : "Reassigned to @#{assignee.username}"
create({
noteable: noteable,
@@ -140,7 +145,7 @@ class Note < ActiveRecord::Base
end
message << ' ' << 'label'.pluralize(labels_count)
- body = "_#{message.capitalize}_"
+ body = "#{message.capitalize}"
create(
noteable: noteable,
@@ -151,7 +156,7 @@ class Note < ActiveRecord::Base
)
end
- def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [])
+ def create_new_commits_note(merge_request, project, author, new_commits, existing_commits = [], oldrev = nil)
total_count = new_commits.length + existing_commits.length
commits_text = ActionController::Base.helpers.pluralize(total_count, 'commit')
body = "Added #{commits_text}:\n\n"
@@ -161,19 +166,23 @@ class Note < ActiveRecord::Base
if existing_commits.length == 1
existing_commits.first.short_id
else
- "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
+ if oldrev
+ "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
+ else
+ "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
+ end
end
commits_text = ActionController::Base.helpers.pluralize(existing_commits.length, 'commit')
- branch =
+ branch =
if merge_request.for_fork?
"#{merge_request.target_project_namespace}:#{merge_request.target_branch}"
else
merge_request.target_branch
end
- message = "* #{commit_ids} - _#{commits_text} from branch `#{branch}`_"
+ message = "* #{commit_ids} - #{commits_text} from branch `#{branch}`"
body << message
body << "\n"
end
@@ -229,14 +238,14 @@ class Note < ActiveRecord::Base
# Determine whether or not a cross-reference note already exists.
def cross_reference_exists?(noteable, mentioner)
- gfm_reference = mentioner_gfm_ref(noteable, mentioner)
+ gfm_reference = mentioner_gfm_ref(noteable, mentioner, true)
notes = if noteable.is_a?(Commit)
- where(commit_id: noteable.id)
+ where(commit_id: noteable.id, noteable_type: 'Commit')
else
- where(noteable_id: noteable.id)
+ where(noteable_id: noteable.id, noteable_type: noteable.class)
end
- notes.where('note like ?', cross_reference_note_content(gfm_reference)).
+ notes.where('note like ?', cross_reference_note_pattern(gfm_reference)).
system.any?
end
@@ -245,55 +254,36 @@ class Note < ActiveRecord::Base
end
def cross_reference_note_prefix
- '_mentioned in '
+ 'mentioned in '
end
private
def cross_reference_note_content(gfm_reference)
- cross_reference_note_prefix + "#{gfm_reference}_"
+ cross_reference_note_prefix + "#{gfm_reference}"
+ end
+
+ def cross_reference_note_pattern(gfm_reference)
+ # Older cross reference notes contained underscores for emphasis
+ "%" + cross_reference_note_content(gfm_reference) + "%"
end
# Prepend the mentioner's namespaced project path to the GFM reference for
# cross-project references. For same-project references, return the
# unmodified GFM reference.
- def mentioner_gfm_ref(noteable, mentioner, project = nil)
- if mentioner.is_a?(Commit)
- if project.nil?
- return mentioner.gfm_reference.sub('commit ', 'commit %')
- else
- mentioning_project = project
- end
- else
- mentioning_project = mentioner.project
- end
-
- noteable_project_id = noteable_project_id(noteable, mentioning_project)
-
- full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
- end
-
- # Return the ID of the project that +noteable+ belongs to, or nil if
- # +noteable+ is a commit and is not part of the project that owns
- # +mentioner+.
- def noteable_project_id(noteable, mentioning_project)
- if noteable.is_a?(Commit)
- if mentioning_project.repository.commit(noteable.id)
- # The noteable commit belongs to the mentioner's project
- mentioning_project.id
- else
- nil
- end
- else
- noteable.project.id
+ def mentioner_gfm_ref(noteable, mentioner, cross_reference = false)
+ if mentioner.is_a?(Commit) && cross_reference
+ return mentioner.gfm_reference.sub('commit ', 'commit %')
end
+
+ full_gfm_reference(mentioner.project, noteable.project, mentioner)
end
# Return the +mentioner+ GFM reference. If the mentioner and noteable
# projects are not the same, add the mentioning project's path to the
# returned value.
- def full_gfm_reference(mentioning_project, noteable_project_id, mentioner)
- if mentioning_project.id == noteable_project_id
+ def full_gfm_reference(mentioning_project, noteable_project, mentioner)
+ if mentioning_project == noteable_project
mentioner.gfm_reference
else
if mentioner.is_a?(Commit)
@@ -311,12 +301,8 @@ class Note < ActiveRecord::Base
end
end
- def commit_author
- @commit_author ||=
- project.team.users.find_by(email: noteable.author_email) ||
- project.team.users.find_by(name: noteable.author_name)
- rescue
- nil
+ def max_attachment_size
+ current_application_settings.max_attachment_size.megabytes.to_i
end
def cross_reference?
@@ -338,7 +324,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
@@ -348,6 +334,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
@@ -441,7 +431,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
@@ -500,7 +490,7 @@ class Note < ActiveRecord::Base
# override to return commits, which are not active record
def noteable
if for_commit?
- project.repository.commit(commit_id)
+ project.commit(commit_id)
else
super
end
diff --git a/app/models/project.rb b/app/models/project.rb
index c45338bf4eb..e866681aab9 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
@@ -89,6 +89,7 @@ class Project < ActiveRecord::Base
has_one :redmine_service, dependent: :destroy
has_one :custom_issue_tracker_service, dependent: :destroy
has_one :gitlab_issue_tracker_service, dependent: :destroy
+ has_one :external_wiki_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
@@ -113,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
@@ -123,23 +126,20 @@ 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 :visibility_level,
- exclusion: { in: gitlab_config.restricted_visibility_levels },
- if: -> { gitlab_config.restricted_visibility_levels.any? }
validates :issues_tracker_id, length: { maximum: 255 }, allow_blank: true
validates :namespace, presence: true
validates_uniqueness_of :name, scope: :namespace_id
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
- 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
@@ -157,7 +157,6 @@ class Project < ActiveRecord::Base
scope :without_user, ->(user) { where('projects.id NOT IN (:ids)', ids: user.authorized_projects.map(&:id) ) }
scope :without_team, ->(team) { team.projects.present? ? where('projects.id NOT IN (:ids)', ids: team.projects.map(&:id)) : scoped }
scope :not_in_group, ->(group) { where('projects.id NOT IN (:ids)', ids: group.project_ids ) }
- scope :in_team, ->(team) { where('projects.id IN (:ids)', ids: team.projects.map(&:id)) }
scope :in_namespace, ->(namespace) { where(namespace_id: namespace.id) }
scope :in_group_namespace, -> { joins(:group) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -188,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
@@ -254,7 +254,11 @@ class Project < ActiveRecord::Base
end
def repository
- @repository ||= Repository.new(path_with_namespace)
+ @repository ||= Repository.new(path_with_namespace, nil, self)
+ end
+
+ def commit(id = 'HEAD')
+ repository.commit(id)
end
def saved?
@@ -265,6 +269,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
@@ -321,14 +329,18 @@ class Project < ActiveRecord::Base
self.id
end
- def issue_exists?(issue_id)
+ def get_issue(issue_id)
if default_issues_tracker?
- self.issues.where(iid: issue_id).first.present?
+ issues.find_by(iid: issue_id)
else
- true
+ ExternalIssue.new(issue_id, self)
end
end
+ def issue_exists?(issue_id)
+ get_issue(issue_id)
+ end
+
def default_issue_tracker
gitlab_issue_tracker_service || create_gitlab_issue_tracker_service
end
@@ -342,11 +354,7 @@ class Project < ActiveRecord::Base
end
def default_issues_tracker?
- if external_issue_tracker
- false
- else
- true
- end
+ !external_issue_tracker
end
def external_issues_trackers
@@ -445,13 +453,13 @@ class Project < ActiveRecord::Base
end
end
- def team_member_by_name_or_email(name = nil, email = nil)
+ def project_member_by_name_or_email(name = nil, email = nil)
user = users.where('name like ? or email like ?', name, email).first
project_members.where(user: user) if user
end
# Get Team Member record by user id
- def team_member_by_id(user_id)
+ def project_member_by_id(user_id)
project_members.find_by(user_id: user_id)
end
@@ -678,11 +686,21 @@ class Project < ActiveRecord::Base
end
def create_repository
- if gitlab_shell.add_repository(path_with_namespace)
- true
+ if forked?
+ if gitlab_shell.fork_repository(forked_from_project.path_with_namespace, self.namespace.path)
+ ensure_satellite_exists
+ true
+ else
+ errors.add(:base, 'Failed to fork repository')
+ false
+ end
else
- errors.add(:base, 'Failed to create repository')
- false
+ if gitlab_shell.add_repository(path_with_namespace)
+ true
+ else
+ errors.add(:base, 'Failed to create repository')
+ false
+ end
end
end
diff --git a/app/models/project_contributions.rb b/app/models/project_contributions.rb
deleted file mode 100644
index 8ab2d814a94..00000000000
--- a/app/models/project_contributions.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-class ProjectContributions
- attr_reader :project, :user
-
- def initialize(project, user)
- @project, @user = project, user
- end
-
- def commits_log
- repository = project.repository
-
- if !repository.exists? || repository.empty?
- return {}
- end
-
- Rails.cache.fetch(cache_key) do
- repository.commits_per_day_for_user(user)
- end
- end
-
- def cache_key
- "#{Date.today.to_s}-commits-log-#{project.id}-#{user.email}"
- end
-end
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
new file mode 100644
index 00000000000..cd3319f077e
--- /dev/null
+++ b/app/models/project_import_data.rb
@@ -0,0 +1,19 @@
+# == Schema Information
+#
+# Table name: project_import_data
+#
+# 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/asana_service.rb b/app/models/project_services/asana_service.rb
index 6a622207385..e6e16058d41 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -77,12 +77,12 @@ automatically inspected. Leave blank to include all branches.'
end
user = data[:user_name]
- branch = data[:ref].gsub('refs/heads/', '')
+ branch = Gitlab::Git.ref_name(data[:ref])
branch_restriction = restrict_to_branch.to_s
# check the branch restriction is poplulated and branch is not included
- if branch_restriction.length > 0 && branch_restriction.index(branch) == nil
+ if branch_restriction.length > 0 && branch_restriction.index(branch).nil?
return
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 0100f1e4a10..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,
@@ -93,7 +93,7 @@ class BambooService < CiService
end
end
- def build_page(sha)
+ def build_page(sha, ref)
build_info(sha) if @response.nil? || !@response.code
if @response.code != 200 || @response['results']['results']['size'] == '0'
@@ -106,7 +106,7 @@ class BambooService < CiService
end
end
- def commit_status(sha)
+ def commit_status(sha, ref)
build_info(sha) if @response.nil? || !@response.code
return :error unless @response.code == 200 || @response.code == 404
diff --git a/app/models/project_services/buildbox_service.rb b/app/models/project_services/buildkite_service.rb
index 270863c1576..a714bc82246 100644
--- a/app/models/project_services/buildbox_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -20,7 +20,9 @@
require "addressable/uri"
-class BuildboxService < CiService
+class BuildkiteService < CiService
+ ENDPOINT = "https://buildkite.com"
+
prop_accessor :project_url, :token
validates :project_url, presence: true, if: :activated?
@@ -29,7 +31,7 @@ class BuildboxService < CiService
after_save :compose_service_hook, if: :activated?
def webhook_url
- "#{buildbox_endpoint('webhook')}/deliver/#{webhook_token}"
+ "#{buildkite_endpoint('webhook')}/deliver/#{webhook_token}"
end
def compose_service_hook
@@ -48,7 +50,7 @@ class BuildboxService < CiService
service_hook.execute(data)
end
- def commit_status(sha)
+ def commit_status(sha, ref)
response = HTTParty.get(commit_status_path(sha), verify: false)
if response.code == 200 && response['status']
@@ -59,10 +61,10 @@ class BuildboxService < CiService
end
def commit_status_path(sha)
- "#{buildbox_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
+ "#{buildkite_endpoint('gitlab')}/status/#{status_token}.json?commit=#{sha}"
end
- def build_page(sha)
+ def build_page(sha, ref)
"#{project_url}/builds?commit=#{sha}"
end
@@ -71,11 +73,11 @@ class BuildboxService < CiService
end
def status_img_path
- "#{buildbox_endpoint('badge')}/#{status_token}.svg"
+ "#{buildkite_endpoint('badge')}/#{status_token}.svg"
end
def title
- 'Buildbox'
+ 'Buildkite'
end
def description
@@ -83,18 +85,18 @@ class BuildboxService < CiService
end
def to_param
- 'buildbox'
+ 'buildkite'
end
def fields
[
{ type: 'text',
name: 'token',
- placeholder: 'Buildbox project GitLab token' },
+ placeholder: 'Buildkite project GitLab token' },
{ type: 'text',
name: 'project_url',
- placeholder: 'https://buildbox.io/example/project' }
+ placeholder: "#{ENDPOINT}/example/project" }
]
end
@@ -116,11 +118,9 @@ class BuildboxService < CiService
end
end
- def buildbox_endpoint(subdomain = nil)
- endpoint = 'https://buildbox.io'
-
+ def buildkite_endpoint(subdomain = nil)
if subdomain.present?
- uri = Addressable::URI.parse(endpoint)
+ uri = Addressable::URI.parse(ENDPOINT)
new_endpoint = "#{uri.scheme || 'http'}://#{subdomain}.#{uri.host}"
if uri.port.present?
@@ -129,7 +129,7 @@ class BuildboxService < CiService
new_endpoint
end
else
- endpoint
+ ENDPOINT
end
end
end
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 1c63444fbf9..e591afdda64 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -64,7 +64,7 @@ class CampfireService < Service
end
def build_message(push)
- ref = push[:ref].gsub("refs/heads/", "")
+ ref = Gitlab::Git.ref_name(push[:ref])
before = push[:before]
after = push[:after]
@@ -72,9 +72,9 @@ class CampfireService < Service
message << "[#{project.name_with_namespace}] "
message << "#{push[:user_name]} "
- if before.include?('000000')
+ if Gitlab::Git.blank_ref?(before)
message << "pushed new branch #{ref} \n"
- elsif after.include?('000000')
+ elsif Gitlab::Git.blank_ref?(after)
message << "removed branch #{ref} \n"
else
message << "pushed #{push[:total_commits_count]} commits to #{ref}. "
diff --git a/app/models/project_services/ci_service.rb b/app/models/project_services/ci_service.rb
index c6f6b4952c9..77d48d4af5e 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
# Base class for CI services
@@ -34,7 +35,7 @@ class CiService < Service
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
#
- def build_page(sha)
+ def build_page(sha, ref)
# implement inside child
end
@@ -51,7 +52,7 @@ class CiService < Service
# # => 'running'
#
#
- def commit_status(sha)
+ def commit_status(sha, ref)
# implement inside child
end
end
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 8d25f627870..7c2027c18e6 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
class CustomIssueTrackerService < IssueTrackerService
diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb
index acb5e7f1af5..8f5d8b086eb 100644
--- a/app/models/project_services/emails_on_push_service.rb
+++ b/app/models/project_services/emails_on_push_service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
class EmailsOnPushService < Service
@@ -36,13 +37,19 @@ class EmailsOnPushService < Service
end
def supported_events
- %w(push)
+ %w(push tag_push)
end
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
- EmailsOnPushWorker.perform_async(project_id, recipients, push_data, send_from_committer_email?, disable_diffs?)
+ EmailsOnPushWorker.perform_async(
+ project_id,
+ recipients,
+ push_data,
+ send_from_committer_email: send_from_committer_email?,
+ disable_diffs: disable_diffs?
+ )
end
def send_from_committer_email?
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
new file mode 100644
index 00000000000..9c46af7e721
--- /dev/null
+++ b/app/models/project_services/external_wiki_service.rb
@@ -0,0 +1,54 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+class ExternalWikiService < Service
+ include HTTParty
+
+ prop_accessor :external_wiki_url
+ validates :external_wiki_url,
+ presence: true,
+ format: { with: /\A#{URI.regexp}\z/ },
+ if: :activated?
+
+ def title
+ 'External Wiki'
+ end
+
+ def description
+ 'Replaces the link to the internal wiki with a link to an external wiki.'
+ end
+
+ def to_param
+ 'external_wiki'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' },
+ ]
+ end
+
+ def execute(_data)
+ @response = HTTParty.get(properties['external_wiki_url'], verify: true) rescue nil
+ if @response !=200
+ nil
+ end
+ end
+end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 99e361dd6ed..bf801ba61ad 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require "flowdock-git-hook"
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index 4e75bdfc953..91ef267ad79 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require "gemnasium/gitlab_service"
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index d81623625c9..949a4d7111b 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -15,9 +15,12 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
class GitlabCiService < CiService
+ API_PREFIX = "api/v1"
+
prop_accessor :project_url, :token
validates :project_url, presence: true, if: :activated?
validates :token, presence: true, if: :activated?
@@ -40,17 +43,17 @@ class GitlabCiService < CiService
service_hook.execute(data)
end
- def commit_status_path(sha)
- project_url + "/commits/#{sha}/status.json?token=#{token}"
+ def commit_status_path(sha, ref)
+ URI::encode(project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}")
end
- def get_ci_build(sha)
+ def get_ci_build(sha, ref)
@ci_builds ||= {}
- @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha), verify: false)
+ @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha, ref), verify: false)
end
- def commit_status(sha)
- response = get_ci_build(sha)
+ def commit_status(sha, ref)
+ response = get_ci_build(sha, ref)
if response.code == 200 and response["status"]
response["status"]
@@ -59,16 +62,36 @@ class GitlabCiService < CiService
end
end
- def commit_coverage(sha)
- response = get_ci_build(sha)
+ 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)
if response.code == 200 and response["coverage"]
response["coverage"]
end
end
- def build_page(sha)
- project_url + "/commits/#{sha}"
+ def build_page(sha, ref)
+ URI::encode(project_url + "/refs/#{ref}/commits/#{sha}")
end
def builds_path
@@ -97,4 +120,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..0ebc0a3ba1a 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -20,8 +20,8 @@
class GitlabIssueTrackerService < IssueTrackerService
include Rails.application.routes.url_helpers
- prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
+ prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def default?
true
@@ -32,20 +32,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/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 90ba7e080f1..38cb64f8c48 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -15,12 +15,13 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
class HipchatService < Service
MAX_COMMITS = 3
- prop_accessor :token, :room, :server
+ prop_accessor :token, :room, :server, :notify, :color, :api_version
validates :token, presence: true, if: :activated?
def title
@@ -39,6 +40,10 @@ class HipchatService < Service
[
{ type: 'text', name: 'token', placeholder: 'Room token' },
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
+ { type: 'checkbox', name: 'notify' },
+ { type: 'select', name: 'color', choices: ['yellow', 'red', 'green', 'purple', 'gray', 'random'] },
+ { type: 'text', name: 'api_version',
+ placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' }
]
@@ -50,18 +55,23 @@ class HipchatService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
-
- gate[room].send('GitLab', create_message(data))
+ message = create_message(data)
+ return unless message.present?
+ gate[room].send('GitLab', message, message_options)
end
private
def gate
- options = { api_version: 'v2' }
+ options = { api_version: api_version || 'v2' }
options[:server_url] = server unless server.blank?
@gate ||= HipChat::Client.new(token, options)
end
+ def message_options
+ { notify: notify.present? && notify == '1', color: color || 'yellow' }
+ end
+
def create_message(data)
object_kind = data[:object_kind]
@@ -79,24 +89,19 @@ class HipchatService < Service
end
def create_push_message(push)
- if push[:ref].starts_with?('refs/tags/')
- ref_type = 'tag'
- ref = push[:ref].gsub('refs/tags/', '')
- else
- ref_type = 'branch'
- ref = push[:ref].gsub('refs/heads/', '')
- end
+ ref_type = Gitlab::Git.tag_ref?(push[:ref]) ? 'tag' : 'branch'
+ ref = Gitlab::Git.ref_name(push[:ref])
before = push[:before]
after = push[:after]
message = ""
message << "#{push[:user_name]} "
- if before.include?('000000')
+ if Gitlab::Git.blank_ref?(before)
message << "pushed new #{ref_type} <a href=\""\
"#{project_url}/commits/#{URI.escape(ref)}\">#{ref}</a>"\
" to #{project_link}\n"
- elsif after.include?('000000')
+ elsif Gitlab::Git.blank_ref?(after)
message << "removed #{ref_type} <b>#{ref}</b> from <a href=\"#{project.web_url}\">#{project_name}</a> \n"
else
message << "pushed to #{ref_type} <a href=\""\
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index deb210c61e4..89f312e8c98 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'uri'
@@ -68,9 +69,15 @@ class IrkerService < Service
'irker'
end
- def execute(push_data)
+ def supported_events
+ %w(push)
+ end
+
+ def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
IrkerWorker.perform_async(project_id, channels,
- colorize_messages, push_data, @settings)
+ colorize_messages, data, @settings)
end
def fields
@@ -142,7 +149,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 16876335b67..c8ab9d63b74 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -30,20 +30,20 @@ class IssueTrackerService < Service
false
end
- def project_url
- # implement inside child
+ def issue_url(iid)
+ self.issues_url.gsub(':id', iid.to_s)
end
- def issues_url
- # implement inside child
+ def project_path
+ project_url
end
- def new_issue_url
- # implement inside child
+ def new_issue_path
+ new_issue_url
end
- def issue_url(iid)
- self.issues_url.gsub(':id', iid.to_s)
+ def issue_path(iid)
+ issue_url(iid)
end
def fields
@@ -60,9 +60,9 @@ class IssueTrackerService < Service
if enabled_in_gitlab_config
self.properties = {
title: issues_tracker['title'],
- project_url: set_project_url,
- issues_url: issues_tracker['issues_url'],
- new_issue_url: issues_tracker['new_issue_url']
+ project_url: add_issues_tracker_id(issues_tracker['project_url']),
+ issues_url: add_issues_tracker_id(issues_tracker['issues_url']),
+ new_issue_url: add_issues_tracker_id(issues_tracker['new_issue_url'])
}
else
self.properties = {}
@@ -111,15 +111,15 @@ class IssueTrackerService < Service
Gitlab.config.issues_tracker[to_param]
end
- def set_project_url
+ def add_issues_tracker_id(url)
if self.project
id = self.project.issues_tracker_id
if id
- issues_tracker['project_url'].gsub(":issues_tracker_id", id)
+ url = url.gsub(":issues_tracker_id", id)
end
end
- issues_tracker['project_url']
+ url
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index fcd9dc2f336..bfa8fc7b860 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -24,13 +24,12 @@ class JiraService < IssueTrackerService
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def help
- issue_tracker_link = help_page_path("integration", "external-issue-tracker")
+ line1 = 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
+ 'allow a user to easily navigate to the Jira issue tracker. See the '\
+ '[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
+ 'for details.'
- line1 = "Setting `project_url`, `issues_url` and `new_issue_url` will "\
- "allow a user to easily navigate to the Jira issue tracker. "\
- "See the [integration doc](#{issue_tracker_link}) for details."
-
- line2 = 'Support for referencing commits and automatic closing of Jira issues directly ' \
+ line2 = 'Support for referencing commits and automatic closing of Jira issues directly '\
'from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)'
[line1, line2].join("\n\n")
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index 0ce324434db..53edf522e9a 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -88,13 +88,13 @@ class PushoverService < Service
def execute(data)
return unless supported_events.include?(data[:object_kind])
- ref = data[:ref].gsub('refs/heads/', '')
+ ref = Gitlab::Git.ref_name(data[:ref])
before = data[:before]
after = data[:after]
- if before.include?('000000')
+ if Gitlab::Git.blank_ref?(before)
message = "#{data[:user_name]} pushed new branch \"#{ref}\"."
- elsif after.include?('000000')
+ elsif Gitlab::Git.blank_ref?(after)
message = "#{data[:user_name]} deleted branch \"#{ref}\"."
else
message = "#{data[:user_name]} push to branch \"#{ref}\"."
diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/slack_service/push_message.rb
index 3dc2df04764..b26f3e9ddce 100644
--- a/app/models/project_services/slack_service/push_message.rb
+++ b/app/models/project_services/slack_service/push_message.rb
@@ -15,13 +15,8 @@ class SlackService
@commits = params.fetch(:commits, [])
@project_name = params[:project_name]
@project_url = params[:project_url]
- if params[:ref].starts_with?('refs/tags/')
- @ref_type = 'tag'
- @ref = params[:ref].gsub('refs/tags/', '')
- else
- @ref_type = 'branch'
- @ref = params[:ref].gsub('refs/heads/', '')
- end
+ @ref_type = Gitlab::Git.tag_ref?(params[:ref]) ? 'tag' : 'branch'
+ @ref = Gitlab::Git.ref_name(params[:ref])
@user_name = params[:user_name]
end
@@ -81,11 +76,11 @@ class SlackService
end
def new_branch?
- before.include?('000000')
+ Gitlab::Git.blank_ref?(before)
end
def removed_branch?
- after.include?('000000')
+ Gitlab::Git.blank_ref?(after)
end
def branch_url
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index 038c200adc7..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,
@@ -88,7 +88,7 @@ class TeamcityService < CiService
@response = HTTParty.get("#{url}", verify: false, basic_auth: auth)
end
- def build_page(sha)
+ def build_page(sha, ref)
build_info(sha) if @response.nil? || !@response.code
if @response.code != 200
@@ -103,7 +103,7 @@ class TeamcityService < CiService
end
end
- def commit_status(sha)
+ def commit_status(sha, ref)
build_info(sha) if @response.nil? || !@response.code
return :error unless @response.code == 200 || @response.code == 404
@@ -132,7 +132,7 @@ class TeamcityService < CiService
password: password,
}
- branch = data[:ref].gsub('refs/heads/', '')
+ branch = Gitlab::Git.ref_name(data[:ref])
self.class.post("#{teamcity_url}/httpAuth/app/rest/buildQueue",
body: "<build branchName=\"#{branch}\">"\
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index bc9c3ce58f6..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
@@ -31,34 +31,31 @@ class ProjectTeam
user
end
- def find_tm(user_id)
- tm = project.project_members.find_by(user_id: user_id)
+ def find_member(user_id)
+ member = project.project_members.find_by(user_id: user_id)
# If user is not in project members
# we should check for group membership
- if group && !tm
- tm = group.group_members.find_by(user_id: user_id)
+ if group && !member
+ member = group.group_members.find_by(user_id: user_id)
end
- tm
+ 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,27 +85,28 @@ class ProjectTeam
@masters ||= fetch_members(:masters)
end
- def import(source_project)
+ def import(source_project, current_user = nil)
target_project = project
- source_team = source_project.project_members.to_a
+ source_members = source_project.project_members.to_a
target_user_ids = target_project.project_members.pluck(:user_id)
- source_team.reject! do |tm|
+ source_members.reject! do |member|
# Skip if user already present in team
- target_user_ids.include?(tm.user_id)
+ !member.invite? && target_user_ids.include?(member.user_id)
end
- source_team.map! do |tm|
- new_tm = tm.dup
- new_tm.id = nil
- new_tm.source = target_project
- new_tm
+ 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
ProjectMember.transaction do
- source_team.each do |tm|
- tm.save
+ source_members.each do |member|
+ member.save
end
end
@@ -118,26 +116,26 @@ class ProjectTeam
end
def guest?(user)
- max_tm_access(user.id) == Gitlab::Access::GUEST
+ max_member_access(user.id) == Gitlab::Access::GUEST
end
def reporter?(user)
- max_tm_access(user.id) == Gitlab::Access::REPORTER
+ max_member_access(user.id) == Gitlab::Access::REPORTER
end
def developer?(user)
- max_tm_access(user.id) == Gitlab::Access::DEVELOPER
+ max_member_access(user.id) == Gitlab::Access::DEVELOPER
end
def master?(user)
- max_tm_access(user.id) == Gitlab::Access::MASTER
+ max_member_access(user.id) == Gitlab::Access::MASTER
end
def member?(user_id)
- !!find_tm(user_id)
+ !!find_member(user_id)
end
- def max_tm_access(user_id)
+ def max_member_access(user_id)
access = []
access << project.project_members.find_by(user_id: user_id).try(:access_field)
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 55438bee245..0706a1ca0d1 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -104,7 +104,7 @@ class ProjectWiki
def page_title_and_dir(title)
title_array = title.split("/")
title = title_array.pop
- [title.gsub(/\.[^.]*$/, ""), title_array.join("/")]
+ [title, title_array.join("/")]
end
def search_files(query)
@@ -112,7 +112,7 @@ class ProjectWiki
end
def repository
- Repository.new(path_with_namespace, default_branch)
+ Repository.new(path_with_namespace, default_branch, @project)
end
def default_branch
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 97207ba1272..8ebd790a89e 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -18,6 +18,6 @@ class ProtectedBranch < ActiveRecord::Base
validates :project, presence: true
def commit
- project.repository.commit(self.name)
+ project.commit(self.name)
end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 5b52739df2b..1b8c74028d9 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1,11 +1,12 @@
class Repository
include Gitlab::ShellAdapter
- attr_accessor :raw_repository, :path_with_namespace
+ attr_accessor :raw_repository, :path_with_namespace, :project
- def initialize(path_with_namespace, default_branch = nil)
+ def initialize(path_with_namespace, default_branch = nil, project = nil)
@path_with_namespace = path_with_namespace
@raw_repository = Gitlab::Git::Repository.new(path_to_repo) if path_with_namespace
+ @project = project
rescue Gitlab::Git::Repository::NoRepository
nil
end
@@ -28,7 +29,7 @@ class Repository
def commit(id = 'HEAD')
return nil unless raw_repository
commit = Gitlab::Git::Commit.find(raw_repository, id)
- commit = Commit.new(commit) if commit
+ commit = Commit.new(commit, @project) if commit
commit
rescue Rugged::OdbError
nil
@@ -42,13 +43,13 @@ class Repository
limit: limit,
offset: offset,
)
- commits = Commit.decorate(commits) if commits.present?
+ commits = Commit.decorate(commits, @project) if commits.present?
commits
end
def commits_between(from, to)
commits = Gitlab::Git::Commit.between(raw_repository, from, to)
- commits = Commit.decorate(commits) if commits.present?
+ commits = Commit.decorate(commits, @project) if commits.present?
commits
end
@@ -62,24 +63,28 @@ class Repository
def add_branch(branch_name, ref)
cache.expire(:branch_names)
+ @branches = nil
gitlab_shell.add_branch(path_with_namespace, branch_name, ref)
end
def add_tag(tag_name, ref, message = nil)
cache.expire(:tag_names)
+ @tags = nil
gitlab_shell.add_tag(path_with_namespace, tag_name, ref, message)
end
def rm_branch(branch_name)
cache.expire(:branch_names)
+ @branches = nil
gitlab_shell.rm_branch(path_with_namespace, branch_name)
end
def rm_tag(tag_name)
cache.expire(:tag_names)
+ @tags = nil
gitlab_shell.rm_tag(path_with_namespace, tag_name)
end
@@ -122,7 +127,7 @@ class Repository
def expire_cache
%i(size branch_names tag_names commit_count graph_log
- readme version contribution_guide).each do |key|
+ readme version contribution_guide changelog license).each do |key|
cache.expire(key)
end
end
@@ -136,8 +141,8 @@ class Repository
commit = Gitlab::Git::Commit.new(rugged_commit)
{
- author_name: commit.author_name.force_encoding('UTF-8'),
- author_email: commit.author_email.force_encoding('UTF-8'),
+ author_name: commit.author_name,
+ author_email: commit.author_email,
additions: commit.stats.additions,
deletions: commit.stats.deletions,
}
@@ -145,28 +150,17 @@ class Repository
end
end
- def timestamps_by_user_log(user)
- args = %W(git log --author=#{user.email} --since=#{(Date.today - 1.year).to_s} --branches --pretty=format:%cd --date=short)
- dates = Gitlab::Popen.popen(args, path_to_repo).first.split("\n")
-
- if dates.present?
- dates
- else
- []
- end
- end
-
- def commits_per_day_for_user(user)
- timestamps_by_user_log(user).
- group_by { |commit_date| commit_date }.
- inject({}) do |hash, (timestamp_date, commits)|
- hash[timestamp_date] = commits.count
- hash
- end
+ def lookup_cache
+ @lookup_cache ||= {}
end
def method_missing(m, *args, &block)
- raw_repository.send(m, *args, &block)
+ if m == :lookup && !block_given?
+ lookup_cache[m] ||= {}
+ lookup_cache[m][args.join(":")] ||= raw_repository.send(m, *args, &block)
+ else
+ raw_repository.send(m, *args, &block)
+ end
end
def respond_to?(method)
@@ -196,16 +190,44 @@ class Repository
end
def contribution_guide
- cache.fetch(:contribution_guide) { tree(:head).contribution_guide }
+ cache.fetch(:contribution_guide) do
+ tree(:head).blobs.find do |file|
+ file.contributing?
+ end
+ end
+ end
+
+ def changelog
+ cache.fetch(:changelog) do
+ tree(:head).blobs.find do |file|
+ file.name =~ /\A(changelog|history)/i
+ end
+ end
+ end
+
+ def license
+ cache.fetch(:license) do
+ tree(:head).blobs.find do |file|
+ file.name =~ /\Alicense/i
+ end
+ end
end
def head_commit
- commit(self.root_ref)
+ @head_commit ||= commit(self.root_ref)
+ end
+
+ def head_tree
+ @head_tree ||= Tree.new(self, head_commit.sha, nil)
end
def tree(sha = :head, path = nil)
if sha == :head
- sha = head_commit.sha
+ if path.nil?
+ return head_tree
+ else
+ sha = head_commit.sha
+ end
end
Tree.new(self, sha, path)
@@ -246,6 +268,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
@@ -333,6 +358,18 @@ class Repository
end
end
+ def branches
+ @branches ||= raw_repository.branches
+ end
+
+ def tags
+ @tags ||= raw_repository.tags
+ end
+
+ def root_ref
+ @root_ref ||= raw_repository.root_ref
+ end
+
private
def cache
diff --git a/app/models/service.rb b/app/models/service.rb
index 33734e97c55..818a6808db5 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
# To add new service you should build a class inherited from Service
@@ -112,7 +113,7 @@ class Service < ActiveRecord::Base
def async_execute(data)
return unless supported_events.include?(data[:object_kind])
-
+
Sidekiq::Client.enqueue(ProjectServiceWorker, id, data)
end
@@ -121,9 +122,27 @@ class Service < ActiveRecord::Base
end
def self.available_services_names
- %w(gitlab_ci campfire hipchat pivotaltracker flowdock assembla asana
- emails_on_push gemnasium slack pushover buildbox bamboo teamcity jira
- redmine custom_issue_tracker irker)
+ %w(
+ asana
+ assembla
+ bamboo
+ buildkite
+ campfire
+ custom_issue_tracker
+ emails_on_push
+ external_wiki
+ flowdock
+ gemnasium
+ gitlab_ci
+ hipchat
+ irker
+ jira
+ pivotaltracker
+ pushover
+ redmine
+ slack
+ teamcity
+ )
end
def self.create_from_template(project_id, template)
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 3fb2ec1d66c..d2af26539b6 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -19,6 +19,7 @@ class Snippet < ActiveRecord::Base
include Sortable
include Linguist::BlobHelper
include Gitlab::VisibilityLevel
+ include Participable
default_value_for :visibility_level, Snippet::PRIVATE
@@ -33,8 +34,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 }
@@ -47,6 +48,8 @@ class Snippet < ActiveRecord::Base
scope :expired, -> { where(["expires_at IS NOT NULL AND expires_at < ?", Time.current]) }
scope :non_expired, -> { where(["expires_at IS NULL OR expires_at > ?", Time.current]) }
+ participant :author, :notes
+
def self.content_types
[
".rb", ".py", ".pl", ".scala", ".c", ".cpp", ".java",
diff --git a/app/models/subscription.rb b/app/models/subscription.rb
new file mode 100644
index 00000000000..dd75d3ab8ba
--- /dev/null
+++ b/app/models/subscription.rb
@@ -0,0 +1,21 @@
+# == Schema Information
+#
+# Table name: subscriptions
+#
+# id :integer not null, primary key
+# user_id :integer
+# subscribable_id :integer
+# subscribable_type :string(255)
+# subscribed :boolean
+# created_at :datetime
+# updated_at :datetime
+#
+
+class Subscription < ActiveRecord::Base
+ belongs_to :user
+ belongs_to :subscribable, polymorphic: true
+
+ validates :user_id,
+ uniqueness: { scope: [:subscribable_id, :subscribable_type] },
+ presence: true
+end
diff --git a/app/models/tree.rb b/app/models/tree.rb
index 4f5d81f0a5e..f279e896cda 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -1,38 +1,38 @@
class Tree
include Gitlab::MarkdownHelper
- attr_accessor :entries, :readme, :contribution_guide
+ attr_accessor :repository, :sha, :path, :entries
def initialize(repository, sha, path = '/')
path = '/' if path.blank?
- git_repo = repository.raw_repository
- @entries = Gitlab::Git::Tree.where(git_repo, sha, path)
-
- available_readmes = @entries.select(&:readme?)
-
- if available_readmes.count > 0
- # If there is more than 1 readme in tree, find readme which is supported
- # by markup renderer.
- if available_readmes.length > 1
- supported_readmes = available_readmes.select do |readme|
- previewable?(readme.name)
- end
-
- # Take the first supported readme, or the first available readme, if we
- # don't support any of them
- readme_tree = supported_readmes.first || available_readmes.first
- else
- readme_tree = available_readmes.first
- end
-
- readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
- @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path)
- end
+
+ @repository = repository
+ @sha = sha
+ @path = path
+
+ git_repo = @repository.raw_repository
+ @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path)
+ end
+
+ def readme
+ return @readme if defined?(@readme)
- if contribution_tree = @entries.find(&:contributing?)
- contribution_path = path == '/' ? contribution_tree.name : File.join(path, contribution_tree.name)
- @contribution_guide = Gitlab::Git::Blob.find(git_repo, sha, contribution_path)
+ available_readmes = blobs.select(&:readme?)
+
+ if available_readmes.count == 0
+ return @readme = nil
end
+
+ # Take the first previewable readme, or the first available readme, if we
+ # can't preview any of them
+ readme_tree = available_readmes.find do |readme|
+ previewable?(readme.name)
+ end || available_readmes.first
+
+ readme_path = path == '/' ? readme_tree.name : File.join(path, readme_tree.name)
+
+ git_repo = repository.raw_repository
+ @readme = Gitlab::Git::Blob.find(git_repo, sha, readme_path)
end
def trees
diff --git a/app/models/user.rb b/app/models/user.rb
index 51dd6332fdb..1cf7cfea974 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -49,6 +49,8 @@
# password_automatically_set :boolean default(FALSE)
# bitbucket_access_token :string(255)
# bitbucket_access_token_secret :string(255)
+# location :string(255)
+# public_email :string(255) default(""), not null
#
require 'carrierwave/orm/activerecord'
@@ -110,6 +112,7 @@ class User < ActiveRecord::Base
has_many :notes, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
has_many :events, dependent: :destroy, foreign_key: :author_id, class_name: "Event"
+ has_many :subscriptions, dependent: :destroy
has_many :recent_events, -> { order "id DESC" }, foreign_key: :author_id, class_name: "Event"
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
@@ -122,26 +125,31 @@ 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? }
validate :avatar_type, if: ->(user) { user.avatar_changed? }
validate :unique_email, if: ->(user) { user.email_changed? }
validate :owns_notification_email, if: ->(user) { user.notification_email_changed? }
+ validate :owns_public_email, if: ->(user) { user.public_email_changed? }
validates :avatar, file_size: { maximum: 200.kilobytes.to_i }
before_validation :generate_password, on: :create
+ before_validation :restricted_signup_domains, 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? }
+ after_update :update_emails_with_primary_email, if: ->(user) { user.email_changed? }
before_save :ensure_authentication_token
after_save :ensure_namespace_correct
after_initialize :set_projects_limit
@@ -154,24 +162,6 @@ class User < ActiveRecord::Base
delegate :path, to: :namespace, allow_nil: true, prefix: true
state_machine :state, initial: :active do
- after_transition any => :blocked do |user, transition|
- # Remove user from all projects and
- user.project_members.find_each do |membership|
- # skip owned resources
- next if membership.project.owner == user
-
- return false unless membership.destroy
- end
-
- # Remove user from all groups
- user.group_members.find_each do |membership|
- # skip owned resources
- next if membership.group.last_owner?(user)
-
- return false unless membership.destroy
- end
- end
-
event :block do
transition active: :blocked
end
@@ -187,11 +177,8 @@ class User < ActiveRecord::Base
scope :admins, -> { where(admin: true) }
scope :blocked, -> { with_state(:blocked) }
scope :active, -> { with_state(:active) }
- scope :in_team, ->(team){ where(id: team.member_ids) }
- scope :not_in_team, ->(team){ where('users.id NOT IN (:ids)', ids: team.member_ids) }
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members)') }
- scope :potential_team_members, ->(team) { team.members.any? ? active.not_in_team(team) : active }
#
# Class methods
@@ -249,22 +236,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
#
@@ -309,13 +280,29 @@ class User < ActiveRecord::Base
end
def unique_email
- self.errors.add(:email, 'has already been taken') if Email.exists?(email: self.email)
+ if !self.emails.exists?(email: self.email) && Email.exists?(email: self.email)
+ self.errors.add(:email, 'has already been taken')
+ end
end
def owns_notification_email
self.errors.add(:notification_email, "is not an email you own") unless self.all_emails.include?(self.notification_email)
end
+ def owns_public_email
+ self.errors.add(:public_email, "is not an email you own") unless self.all_emails.include?(self.public_email)
+ end
+
+ def update_emails_with_primary_email
+ primary_email_record = self.emails.find_by(email: self.email)
+ if primary_email_record
+ primary_email_record.destroy
+ self.emails.create(email: self.email_was)
+
+ self.update_secondary_emails!
+ end
+ end
+
# Groups user has access to
def authorized_groups
@authorized_groups ||= begin
@@ -425,7 +412,7 @@ class User < ActiveRecord::Base
end
def tm_of(project)
- project.team_member_by_id(self.id)
+ project.project_member_by_id(self.id)
end
def already_forked?(project)
@@ -450,8 +437,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
@@ -471,6 +466,18 @@ 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 = nil
+ end
+ end
+
+ def update_secondary_emails!
+ self.set_notification_email
+ self.set_public_email
+ self.save if self.notification_email_changed? || self.public_email_changed?
+ end
+
def set_projects_limit
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined
@@ -522,13 +529,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
@@ -624,13 +631,33 @@ class User < ActiveRecord::Base
end
def contributed_projects_ids
- Event.where(author_id: self).
+ Event.contributions.where(author_id: self).
where("created_at > ?", Time.now - 1.year).
- where("action = :pushed OR (target_type = 'MergeRequest' AND action = :created)",
- pushed: Event::PUSHED, created: Event::CREATED).
reorder(project_id: :desc).
select(:project_id).
- uniq
- .map(&:project_id)
+ uniq.map(&:project_id)
+ end
+
+ def restricted_signup_domains
+ email_domains = current_application_settings.restricted_signup_domains
+
+ unless email_domains.blank?
+ match_found = email_domains.any? do |domain|
+ escaped = Regexp.escape(domain).gsub('\*','.*?')
+ regexp = Regexp.new "^#{escaped}$", Regexp::IGNORECASE
+ email_domain = Mail::Address.new(self.email).domain
+ email_domain =~ regexp
+ end
+
+ unless match_found
+ self.errors.add :email,
+ 'is not whitelisted. ' +
+ 'Email domains valid for registration are: ' +
+ email_domains.join(', ')
+ return false
+ end
+ end
+
+ true
end
end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 32981a0e664..e9413c34bae 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -179,7 +179,8 @@ class WikiPage
if valid? && project_wiki.send(method, *args)
page_details = if method == :update_page
- @page.path
+ # Use url_path instead of path to omit format extension
+ @page.url_path
else
title
end
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/base_service.rb b/app/services/base_service.rb
index 52ab29f1492..6d9ed345914 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -31,8 +31,19 @@ class BaseService
SystemHooksService.new
end
- def current_application_settings
- ApplicationSetting.current
+ # Add an error to the specified model for restricted visibility levels
+ def deny_visibility_level(model, denied_visibility_level = nil)
+ denied_visibility_level ||= model.visibility_level
+
+ level_name = 'Unknown'
+ Gitlab::VisibilityLevel.options.each do |name, level|
+ level_name = name if level == denied_visibility_level
+ end
+
+ model.errors.add(
+ :visibility_level,
+ "#{level_name} visibility has been restricted by your GitLab administrator"
+ )
end
private
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 5e971c7891c..cf7ae4345f3 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -17,10 +17,15 @@ class CreateBranchService < BaseService
new_branch = repository.find_branch(branch_name)
if new_branch
- EventCreateService.new.push_ref(project, current_user, new_branch, 'add')
- return success(new_branch)
+ push_data = build_push_data(project, current_user, new_branch)
+
+ EventCreateService.new.push(project, current_user, push_data)
+ project.execute_hooks(push_data.dup, :push_hooks)
+ project.execute_services(push_data.dup, :push_hooks)
+
+ success(new_branch)
else
- return error('Invalid reference name')
+ error('Invalid reference name')
end
end
@@ -29,4 +34,9 @@ class CreateBranchService < BaseService
out[:branch] = branch
out
end
+
+ def build_push_data(project, user, branch)
+ Gitlab::PushDataBuilder.
+ build(project, user, Gitlab::Git::BLANK_SHA, branch.target, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
+ end
end
diff --git a/app/services/create_snippet_service.rb b/app/services/create_snippet_service.rb
new file mode 100644
index 00000000000..101a3df5eee
--- /dev/null
+++ b/app/services/create_snippet_service.rb
@@ -0,0 +1,20 @@
+class CreateSnippetService < BaseService
+ def execute
+ if project.nil?
+ snippet = PersonalSnippet.new(params)
+ else
+ snippet = project.snippets.build(params)
+ end
+
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user,
+ params[:visibility_level])
+ deny_visibility_level(snippet)
+ return snippet
+ end
+
+ snippet.author = current_user
+
+ snippet.save
+ snippet
+ end
+end
diff --git a/app/services/create_tag_service.rb b/app/services/create_tag_service.rb
index 8cd65724cb9..1a7318048b3 100644
--- a/app/services/create_tag_service.rb
+++ b/app/services/create_tag_service.rb
@@ -13,17 +13,15 @@ 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)
if new_tag
- EventCreateService.new.push_ref(project, current_user, new_tag, 'add', 'refs/tags')
-
push_data = create_push_data(project, current_user, new_tag)
+
+ EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
@@ -40,9 +38,8 @@ class CreateTagService < BaseService
end
def create_push_data(project, user, tag)
- data = Gitlab::PushDataBuilder.
- build(project, user, Gitlab::Git::BLANK_SHA, tag.target, 'refs/tags/' + tag.name, [])
- data[:object_kind] = "tag_push"
- data
+ commits = [project.commit(tag.target)].compact
+ Gitlab::PushDataBuilder.
+ build(project, user, Gitlab::Git::BLANK_SHA, tag.target, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", commits, tag.message)
end
end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index c26aee2b0aa..b19b112a0c4 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -25,10 +25,15 @@ class DeleteBranchService < BaseService
end
if repository.rm_branch(branch_name)
- EventCreateService.new.push_ref(project, current_user, branch, 'rm')
+ push_data = build_push_data(branch)
+
+ EventCreateService.new.push(project, current_user, push_data)
+ project.execute_hooks(push_data.dup, :push_hooks)
+ project.execute_services(push_data.dup, :push_hooks)
+
success('Branch was removed')
else
- return error('Failed to remove branch')
+ error('Failed to remove branch')
end
end
@@ -43,4 +48,9 @@ class DeleteBranchService < BaseService
out[:message] = message
out
end
+
+ def build_push_data(branch)
+ Gitlab::PushDataBuilder
+ .build(project, current_user, branch.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}", [])
+ end
end
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
new file mode 100644
index 00000000000..0c836401136
--- /dev/null
+++ b/app/services/delete_tag_service.rb
@@ -0,0 +1,42 @@
+require_relative 'base_service'
+
+class DeleteTagService < BaseService
+ def execute(tag_name)
+ repository = project.repository
+ tag = repository.find_tag(tag_name)
+
+ # No such tag
+ unless tag
+ return error('No such tag', 404)
+ end
+
+ if repository.rm_tag(tag_name)
+ push_data = build_push_data(tag)
+
+ EventCreateService.new.push(project, current_user, push_data)
+ project.execute_hooks(push_data.dup, :tag_push_hooks)
+ project.execute_services(push_data.dup, :tag_push_hooks)
+
+ success('Tag was removed')
+ else
+ error('Failed to remove tag')
+ end
+ end
+
+ def error(message, return_code = 400)
+ out = super(message)
+ out[:return_code] = return_code
+ out
+ end
+
+ def success(message)
+ out = super()
+ out[:message] = message
+ out
+ end
+
+ def build_push_data(tag)
+ Gitlab::PushDataBuilder
+ .build(project, current_user, tag.target, Gitlab::Git::BLANK_SHA, "#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}", [])
+ end
+end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index ba9547b9242..103d6b0a08b 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -62,26 +62,6 @@ class EventCreateService
create_event(project, current_user, Event::CREATED)
end
- def push_ref(project, current_user, ref, action = 'add', prefix = 'refs/heads')
- commit = project.repository.commit(ref.target)
-
- if action.to_s == 'add'
- before = '00000000'
- after = commit.id
- else
- before = commit.id
- after = '00000000'
- end
-
- data = {
- ref: "#{prefix}/#{ref.name}",
- before: before,
- after: after
- }
-
- push(project, current_user, data)
- end
-
def push(project, current_user, push_data)
create_event(project, current_user, Event::PUSHED, data: push_data)
end
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index de5322e990a..23833aa78ec 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class CreateService < BaseService
def execute
- allowed = Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref)
+ allowed = Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
unless allowed
return error("You are not allowed to create file in this branch")
@@ -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/files/delete_service.rb b/app/services/files/delete_service.rb
index 8e73c2e2727..1497a0f883b 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class DeleteService < BaseService
def execute
- allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref)
+ allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
unless allowed
return error("You are not allowed to push into this branch")
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 328cf3a4b06..0724d3ae634 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -3,7 +3,7 @@ require_relative "base_service"
module Files
class UpdateService < BaseService
def execute
- allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, project, ref)
+ allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(ref)
unless allowed
return error("You are not allowed to push into this branch")
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 4e1afea6d50..bdf36af02fd 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -23,41 +23,44 @@ class GitPushService
project.repository.expire_cache
project.update_repository_size
- if push_to_branch?(ref)
- if push_remove_branch?(ref, newrev)
- @push_commits = []
- elsif push_to_new_branch?(ref, oldrev)
- # Re-find the pushed commits.
- if is_default_branch?(ref)
- # Initial push to the default branch. Take the full history of that branch as "newly pushed".
- @push_commits = project.repository.commits(newrev)
-
- # Set protection on the default branch if configured
- if (current_application_settings.default_branch_protection != PROTECTION_NONE)
- developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
- project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push })
- end
- else
- # Use the pushed commits that aren't reachable by the default branch
- # as a heuristic. This may include more commits than are actually pushed, but
- # that shouldn't matter because we check for existing cross-references later.
- @push_commits = project.repository.commits_between(project.default_branch, newrev)
-
- # don't process commits for the initial push to the default branch
- process_commit_messages(ref)
+ if push_remove_branch?(ref, newrev)
+ @push_commits = []
+ elsif push_to_new_branch?(ref, oldrev)
+ # Re-find the pushed commits.
+ if is_default_branch?(ref)
+ # Initial push to the default branch. Take the full history of that branch as "newly pushed".
+ @push_commits = project.repository.commits(newrev)
+
+ # Ensure HEAD points to the default branch in case it is not master
+ branch_name = Gitlab::Git.ref_name(ref)
+ project.change_head(branch_name)
+
+ # Set protection on the default branch if configured
+ if (current_application_settings.default_branch_protection != PROTECTION_NONE)
+ developers_can_push = current_application_settings.default_branch_protection == PROTECTION_DEV_CAN_PUSH ? true : false
+ project.protected_branches.create({ name: project.default_branch, developers_can_push: developers_can_push })
end
- elsif push_to_existing_branch?(ref, oldrev)
- # Collect data for this git push
- @push_commits = project.repository.commits_between(oldrev, newrev)
- project.update_merge_requests(oldrev, newrev, ref, @user)
+ else
+ # Use the pushed commits that aren't reachable by the default branch
+ # as a heuristic. This may include more commits than are actually pushed, but
+ # that shouldn't matter because we check for existing cross-references later.
+ @push_commits = project.repository.commits_between(project.default_branch, newrev)
+
+ # don't process commits for the initial push to the default branch
process_commit_messages(ref)
end
-
- @push_data = post_receive_data(oldrev, newrev, ref)
- EventCreateService.new.push(project, user, @push_data)
- project.execute_hooks(@push_data.dup, :push_hooks)
- project.execute_services(@push_data.dup, :push_hooks)
+ elsif push_to_existing_branch?(ref, oldrev)
+ # Collect data for this git push
+ @push_commits = project.repository.commits_between(oldrev, newrev)
+ project.update_merge_requests(oldrev, newrev, ref, @user)
+ process_commit_messages(ref)
end
+
+ @push_data = build_push_data(oldrev, newrev, ref)
+
+ EventCreateService.new.push(project, user, @push_data)
+ project.execute_hooks(@push_data.dup, :push_hooks)
+ project.execute_services(@push_data.dup, :push_hooks)
end
protected
@@ -71,7 +74,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(user)
# Load commit author only if needed.
# For push with 1k commits it prevents 900+ requests in database
@@ -88,52 +91,46 @@ 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?
author ||= commit_user(commit)
refs.each do |r|
- Note.create_cross_reference_note(r, commit, author, project)
+ Note.create_cross_reference_note(r, commit, author)
end
end
end
end
- def post_receive_data(oldrev, newrev, ref)
+ def build_push_data(oldrev, newrev, ref)
Gitlab::PushDataBuilder.
build(project, user, oldrev, newrev, ref, push_commits)
end
def push_to_existing_branch?(ref, oldrev)
- ref_parts = ref.split('/')
-
# Return if this is not a push to a branch (e.g. new commits)
- ref_parts[1].include?('heads') && oldrev != Gitlab::Git::BLANK_SHA
+ Gitlab::Git.branch_ref?(ref) && !Gitlab::Git.blank_ref?(oldrev)
end
def push_to_new_branch?(ref, oldrev)
- ref_parts = ref.split('/')
-
- ref_parts[1].include?('heads') && oldrev == Gitlab::Git::BLANK_SHA
+ Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(oldrev)
end
def push_remove_branch?(ref, newrev)
- ref_parts = ref.split('/')
-
- ref_parts[1].include?('heads') && newrev == Gitlab::Git::BLANK_SHA
+ Gitlab::Git.branch_ref?(ref) && Gitlab::Git.blank_ref?(newrev)
end
def push_to_branch?(ref)
- ref.include?('refs/heads')
+ Gitlab::Git.branch_ref?(ref)
end
def is_default_branch?(ref)
- ref == "refs/heads/#{project.default_branch}"
+ Gitlab::Git.branch_ref?(ref) && Gitlab::Git.ref_name(ref) == project.default_branch
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/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index cd92f50b02a..075a6118da2 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -3,21 +3,35 @@ class GitTagPushService
def execute(project, user, oldrev, newrev, ref)
@project, @user = project, user
- @push_data = create_push_data(oldrev, newrev, ref)
+
+ @push_data = build_push_data(oldrev, newrev, ref)
EventCreateService.new.push(project, user, @push_data)
- project.repository.expire_cache
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
+ project.repository.expire_cache
+
true
end
private
- def create_push_data(oldrev, newrev, ref)
- data = Gitlab::PushDataBuilder.build(project, user, oldrev, newrev, ref, [])
- data[:object_kind] = "tag_push"
- data
+ def build_push_data(oldrev, newrev, ref)
+ commits = []
+ message = nil
+
+ if !Gitlab::Git.blank_ref?(newrev)
+ tag_name = Gitlab::Git.ref_name(ref)
+ tag = project.repository.find_tag(tag_name)
+ if tag && tag.target == newrev
+ commit = project.commit(tag.target)
+ commits = [commit].compact
+ message = tag.message
+ end
+ end
+
+ Gitlab::PushDataBuilder.
+ build(project, user, oldrev, newrev, ref, commits, message)
end
end
diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb
index c7cd20b6b60..eb07413ee94 100644
--- a/app/services/issues/bulk_update_service.rb
+++ b/app/services/issues/bulk_update_service.rb
@@ -4,9 +4,9 @@ module Issues
issues_ids = params.delete(:issues_ids).split(",")
issue_params = params
- issue_params.delete(:state_event) unless issue_params[:state_event].present?
- issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present?
- issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present?
+ issue_params.delete(:state_event) unless issue_params[:state_event].present?
+ issue_params.delete(:milestone_id) unless issue_params[:milestone_id].present?
+ issue_params.delete(:assignee_id) unless issue_params[:assignee_id].present?
issues = Issue.where(id: issues_ids)
issues.each do |issue|
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index c61d67a7893..8f04a69287a 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -14,6 +14,9 @@ module Issues
issue.update_nth_task(params[:task_num].to_i, false)
end
+ params[:assignee_id] = "" if params[:assignee_id] == IssuableFinder::NONE
+ params[:milestone_id] = "" if params[:milestone_id] == IssuableFinder::NONE
+
old_labels = issue.labels.to_a
if params.present? && issue.update_attributes(params.except(:state_event,
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index a44b91166e8..956480938c3 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -29,7 +29,7 @@ module MergeRequests
# At this point we decide if merge request can be created
# If we have at least one commit to merge -> creation allowed
if commits.present?
- merge_request.compare_commits = Commit.decorate(commits)
+ merge_request.compare_commits = Commit.decorate(commits, merge_request.source_project)
merge_request.can_be_created = true
merge_request.compare_failed = false
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index ea846472766..e9b526d1fb7 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -1,10 +1,10 @@
module MergeRequests
class RefreshService < MergeRequests::BaseService
def execute(oldrev, newrev, ref)
- return true unless ref =~ /heads/
+ return true unless Gitlab::Git.branch_ref?(ref)
@oldrev, @newrev = oldrev, newrev
- @branch_name = ref.gsub("refs/heads/", "")
+ @branch_name = Gitlab::Git.ref_name(ref)
@fork_merge_requests = @project.fork_merge_requests.opened
@commits = @project.repository.commits_between(oldrev, newrev)
@@ -53,7 +53,7 @@ module MergeRequests
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_code
- update_merge_request(merge_request)
+ merge_request.mark_as_unchecked
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
@@ -61,20 +61,14 @@ module MergeRequests
if matches.any?
merge_request.reload_code
- update_merge_request(merge_request)
+ merge_request.mark_as_unchecked
else
- update_merge_request(merge_request)
+ merge_request.mark_as_unchecked
end
end
end
end
- def update_merge_request(merge_request)
- MergeRequests::UpdateService.new(
- merge_request.target_project,
- @current_user, merge_status: 'unchecked').execute(merge_request)
- end
-
# Add comment about pushing new commits to merge requests
def comment_mr_with_commits
merge_requests = @project.origin_merge_requests.opened.where(source_branch: @branch_name).to_a
@@ -89,7 +83,7 @@ module MergeRequests
end
Note.create_new_commits_note(merge_request, merge_request.project,
- @current_user, new_commits, existing_commits)
+ @current_user, new_commits, existing_commits, @oldrev)
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 870b50bb60d..23af2656c37 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -23,6 +23,9 @@ module MergeRequests
merge_request.update_nth_task(params[:task_num].to_i, false)
end
+ 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
if params.present? && merge_request.update_attributes(
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index e969061f229..d19a6c2eca3 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -15,7 +15,7 @@ module Notes
# Create a cross-reference note if this Note contains GFM that names an
# issue, merge request, or commit.
note.references.each do |mentioned|
- Note.create_cross_reference_note(mentioned, note.noteable, note.author, note.project)
+ Note.create_cross_reference_note(mentioned, note.noteable, note.author)
end
execute_hooks(note)
diff --git a/app/services/notes/update_service.rb b/app/services/notes/update_service.rb
index 63431b82471..45a0db761ec 100644
--- a/app/services/notes/update_service.rb
+++ b/app/services/notes/update_service.rb
@@ -13,8 +13,7 @@ module Notes
# Create a cross-reference note if this Note contains GFM that
# names an issue, merge request, or commit.
note.references.each do |mentioned|
- Note.create_cross_reference_note(mentioned, note.noteable,
- note.author, note.project)
+ Note.create_cross_reference_note(mentioned, note.noteable, note.author)
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 2fc63b9f4b7..0d7ffbeebd9 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -92,6 +92,8 @@ class NotificationService
#
def merge_mr(merge_request, current_user)
recipients = reject_muted_users([merge_request.author, merge_request.assignee], merge_request.target_project)
+ recipients = add_subscribed_users(recipients, merge_request)
+ recipients = reject_unsubscribed_users(recipients, merge_request)
recipients = recipients.concat(project_watchers(merge_request.target_project)).uniq
recipients.delete(current_user)
@@ -118,39 +120,36 @@ class NotificationService
return true unless note.noteable_type.present?
# ignore gitlab service messages
- return true if note.note.start_with?('_Status changed to closed_')
+ 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
-
- 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
+ recipients = []
- # 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(note.author)
+ 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
# Reject mutes users
recipients = reject_muted_users(recipients, note.project)
+ recipients = add_subscribed_users(recipients, note.noteable)
+
+ recipients = reject_unsubscribed_users(recipients, note.noteable)
+
# Reject author
recipients.delete(note.author)
@@ -162,20 +161,44 @@ class NotificationService
end
end
- def new_team_member(project_member)
+ 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
- def update_team_member(project_member)
+ def update_project_member(project_member)
mailer.project_access_granted_email(project_member.id)
end
- def new_group_member(users_group)
- mailer.group_access_granted_email(users_group.id)
+ 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
- def update_group_member(users_group)
- mailer.group_access_granted_email(users_group.id)
+ def update_group_member(group_member)
+ mailer.group_access_granted_email(group_member.id)
end
def project_was_moved(project)
@@ -194,11 +217,11 @@ class NotificationService
project_members = project_member_notification(project)
users_with_project_level_global = project_member_notification(project, Notification::N_GLOBAL)
- users_with_group_level_global = users_group_notification(project, Notification::N_GLOBAL)
+ users_with_group_level_global = group_member_notification(project, Notification::N_GLOBAL)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
- users_with_group_setting = select_users_group_setting(project, project_members, users_with_group_level_global, users)
+ users_with_group_setting = select_group_member_setting(project, project_members, users_with_group_level_global, users)
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
@@ -213,7 +236,7 @@ class NotificationService
end
end
- def users_group_notification(project, notification_level)
+ def group_member_notification(project, notification_level)
if project.group
project.group.group_members.where(notification_level: notification_level).pluck(:user_id)
else
@@ -243,8 +266,8 @@ class NotificationService
end
# Build a list of users based on group notification settings
- def select_users_group_setting(project, project_members, global_setting, users_global_level_watch)
- uids = users_group_notification(project, Notification::N_WATCH)
+ def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
+ uids = group_member_notification(project, Notification::N_WATCH)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -268,24 +291,25 @@ class NotificationService
# Also remove duplications and nil recipients
def reject_muted_users(users, project = nil)
users = users.to_a.compact.uniq
+ users = users.reject(&:blocked?)
users.reject do |user|
next user.notification.disabled? unless project
- tm = project.project_members.find_by(user_id: user.id)
+ member = project.project_members.find_by(user_id: user.id)
- if !tm && project.group
- tm = project.group.group_members.find_by(user_id: user.id)
+ if !member && project.group
+ member = project.group.group_members.find_by(user_id: user.id)
end
# reject users who globally disabled notification and has no membership
- next user.notification.disabled? unless tm
+ next user.notification.disabled? unless member
# reject users who disabled notification in project
- next true if tm.notification.disabled?
+ next true if member.notification.disabled?
# reject users who have N_GLOBAL in project and disabled in global settings
- tm.notification.global? && user.notification.disabled?
+ member.notification.global? && user.notification.disabled?
end
end
@@ -296,23 +320,44 @@ class NotificationService
users.reject do |user|
next user.notification.mention? unless project
- tm = project.project_members.find_by(user_id: user.id)
+ member = project.project_members.find_by(user_id: user.id)
- if !tm && project.group
- tm = project.group.group_members.find_by(user_id: user.id)
+ if !member && project.group
+ member = project.group.group_members.find_by(user_id: user.id)
end
# reject users who globally set mention notification and has no membership
- next user.notification.mention? unless tm
+ next user.notification.mention? unless member
# reject users who set mention notification in project
- next true if tm.notification.mention?
+ next true if member.notification.mention?
# reject users who have N_MENTION in project and disabled in global settings
- tm.notification.global? && user.notification.mention?
+ member.notification.global? && user.notification.mention?
end
end
+ def reject_unsubscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscriptions
+
+ recipients.reject do |user|
+ subscription = target.subscriptions.find_by_user_id(user.id)
+ subscription && !subscription.subscribed
+ end
+ end
+
+ def add_subscribed_users(recipients, target)
+ return recipients unless target.respond_to? :subscriptions
+
+ subscriptions = target.subscriptions
+
+ if subscriptions.any?
+ recipients + subscriptions.where(subscribed: true).map(&:user)
+ else
+ recipients
+ end
+ end
+
def new_resource_email(target, project, method)
recipients = build_recipients(target, project)
recipients.delete(target.author)
@@ -360,7 +405,9 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients = reject_mention_users(recipients, project)
+ recipients = add_subscribed_users(recipients, target)
recipients = recipients.concat(project_watchers(project)).uniq
+ recipients = reject_unsubscribed_users(recipients, target)
recipients
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 4fe790b98f1..011f6f6145e 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -5,11 +5,16 @@ module Projects
end
def execute
+ forked_from_project_id = params.delete(:forked_from_project_id)
+
@project = Project.new(params)
- # Reset visibility level if is not allowed to set it
- unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
- @project.visibility_level = default_features.visibility_level
+ # Make sure that the user is allowed to use the specified visibility
+ # level
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user,
+ params[:visibility_level])
+ deny_visibility_level(@project)
+ return @project
end
# Set project name from path
@@ -42,10 +47,14 @@ module Projects
@project.creator = current_user
+ if forked_from_project_id
+ @project.build_forked_project_link(forked_from_project_id: forked_from_project_id)
+ end
+
Project.transaction do
@project.save
- unless @project.import?
+ if @project.persisted? && !@project.import?
unless @project.create_repository
raise 'Failed to create repository'
end
@@ -80,7 +89,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..50f208b11d1 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -1,60 +1,28 @@
module Projects
class ForkService < BaseService
- include Gitlab::ShellAdapter
-
def execute
- @from_project = @project
-
- project_params = {
- visibility_level: @from_project.visibility_level,
- description: @from_project.description,
+ new_params = {
+ forked_from_project_id: @project.id,
+ visibility_level: @project.visibility_level,
+ description: @project.description,
+ name: @project.name,
+ path: @project.path,
+ namespace_id: @params[:namespace].try(:id) || current_user.namespace.id
}
- project = Project.new(project_params)
- project.name = @from_project.name
- project.path = @from_project.path
- project.creator = @current_user
- if @from_project.avatar.present? && @from_project.avatar.image?
- project.avatar = @from_project.avatar
+ if @project.avatar.present? && @project.avatar.image?
+ new_params[:avatar] = @project.avatar
end
- if namespace = @params[:namespace]
- project.namespace = namespace
- else
- project.namespace = @current_user.namespace
- end
-
- unless @current_user.can?(:create_projects, project.namespace)
- project.errors.add(:namespace, 'insufficient access rights')
- return project
- end
+ new_project = CreateService.new(current_user, new_params).execute
- # If the project cannot save, we do not want to trigger the project destroy
- # as this can have the side effect of deleting a repo attached to an existing
- # project with the same name and namespace
- if project.valid?
- begin
- Project.transaction do
- #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]
- 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
- rescue => ex
- project.errors.add(:base, 'Fork transaction failed.')
- project.destroy
+ if new_project.persisted?
+ if @project.gitlab_ci?
+ ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token)
end
- else
- project.errors.add(:base, 'Invalid fork destination')
end
- project
+ new_project
end
end
end
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index f6f9aceef95..b91590a1a90 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,25 +7,25 @@ module Projects
else
[]
end
- team_members = sorted(@project.team.members)
- participants = all_members + groups + team_members + participating
+ project_members = sorted(project.team.members)
+ participants = all_members + groups + project_members + participating
participants.uniq
end
def participants_in(type, id)
- users = case type
- when "Issue"
- issue = @project.issues.find_by_iid(id)
- issue ? issue.participants : []
- when "MergeRequest"
- merge_request = @project.merge_requests.find_by_iid(id)
- merge_request ? merge_request.participants : []
- when "Commit"
- author_ids = Note.for_commit_id(id).pluck(:author_id).uniq
- User.where(id: author_ids)
- else
- []
- end
+ target =
+ case type
+ when "Issue"
+ project.issues.find_by_iid(id)
+ when "MergeRequest"
+ project.merge_requests.find_by_iid(id)
+ when "Commit"
+ project.commit(id)
+ end
+
+ return [] unless target
+
+ users = target.participants(current_user)
sorted(users)
end
@@ -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/transfer_service.rb b/app/services/projects/transfer_service.rb
index 3372cfc11d0..489e03bd5ef 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -43,6 +43,9 @@ module Projects
project.namespace = new_namespace
project.save!
+ # Notifications
+ project.send_move_instructions
+
# Move main repository
unless gitlab_shell.mv_repository(old_path, new_path)
raise TransferError.new('Cannot move project')
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 36877a61679..69bdd045ddf 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -2,8 +2,13 @@ module Projects
class UpdateService < BaseService
def execute
# check that user is allowed to set specified visibility_level
- unless can?(current_user, :change_visibility_level, project) && Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
- params[:visibility_level] = project.visibility_level
+ new_visibility = params[:visibility_level]
+ if new_visibility && new_visibility.to_i != project.visibility_level
+ unless can?(current_user, :change_visibility_level, project) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ deny_visibility_level(project, new_visibility)
+ return project
+ end
end
new_branch = params[:default_branch]
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/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 46f6e91e808..c5d0b08845b 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -41,7 +41,7 @@ class SystemHooksService
path_with_namespace: model.path_with_namespace,
project_id: model.id,
owner_name: owner.name,
- owner_email: owner.respond_to?(:email) ? owner.email : nil,
+ owner_email: owner.respond_to?(:email) ? owner.email : "",
project_visibility: Project.visibility_levels.key(model.visibility_level_field).downcase
})
when User
diff --git a/app/services/update_snippet_service.rb b/app/services/update_snippet_service.rb
new file mode 100644
index 00000000000..9d181c2d2ab
--- /dev/null
+++ b/app/services/update_snippet_service.rb
@@ -0,0 +1,22 @@
+class UpdateSnippetService < BaseService
+ attr_accessor :snippet
+
+ def initialize(project, user, snippet, params)
+ super(project, user, params)
+ @snippet = snippet
+ end
+
+ def execute
+ # check that user is allowed to set specified visibility_level
+ new_visibility = params[:visibility_level]
+ if new_visibility && new_visibility.to_i != snippet.visibility_level
+ unless can?(current_user, :change_visibility_level, snippet) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ deny_visibility_level(snippet, new_visibility)
+ return snippet
+ end
+ end
+
+ snippet.update_attributes(params)
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index cab5688a499..4ceae814805 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -8,22 +8,30 @@
%fieldset
%legend Features
.form-group
- = f.label :signup_enabled, class: 'control-label col-sm-2'
- .col-sm-10
- = f.check_box :signup_enabled, class: 'checkbox form-control'
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :signup_enabled do
+ = f.check_box :signup_enabled
+ Signup enabled
.form-group
- = f.label :signin_enabled, class: 'control-label col-sm-2'
- .col-sm-10
- = f.check_box :signin_enabled, class: 'checkbox form-control'
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :signin_enabled do
+ = f.check_box :signin_enabled
+ Signin enabled
.form-group
- = f.label :gravatar_enabled, class: 'control-label col-sm-2'
- .col-sm-10
- = f.check_box :gravatar_enabled, class: 'checkbox form-control'
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :gravatar_enabled do
+ = f.check_box :gravatar_enabled
+ Gravatar enabled
.form-group
- = f.label :twitter_sharing_enabled, "Twitter enabled", class: 'control-label col-sm-2'
- .col-sm-10
- = f.check_box :twitter_sharing_enabled, class: 'checkbox form-control', :'aria-describedby' => 'twitter_help_block'
- %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :twitter_sharing_enabled do
+ = f.check_box :twitter_sharing_enabled, :'aria-describedby' => 'twitter_help_block'
+ %strong Twitter enabled
+ %span.help-block#twitter_help_block Show users a button to share their newly created public or internal projects on twitter
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
@@ -40,6 +48,22 @@
= f.label :default_branch_protection, class: 'control-label col-sm-2'
.col-sm-10
= f.select :default_branch_protection, options_for_select(Gitlab::Access.protection_options, @application_setting.default_branch_protection), {}, class: 'form-control'
+ .form-group.project-visibility-level-holder
+ = f.label :default_project_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: 'Project')
+ .form-group.project-visibility-level-holder
+ = f.label :default_snippet_visibility, class: 'control-label col-sm-2'
+ .col-sm-10
+ = render('shared/visibility_radios', model_method: :default_snippet_visibility, form: f, selected_level: @application_setting.default_snippet_visibility, form_model: 'Snippet')
+ .form-group
+ = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
+ .col-sm-10
+ - data_attrs = { toggle: 'buttons' }
+ .btn-group{ data: data_attrs }
+ - restricted_level_checkboxes('restricted-visibility-help').each do |level|
+ = level
+ %span.help-block#restricted-visibility-help Selected levels cannot be used by non-admin users for projects or snippets
.form-group
= f.label :home_page_url, class: 'control-label col-sm-2'
.col-sm-10
@@ -50,5 +74,15 @@
.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-group
+ = f.label :restricted_signup_domains, 'Restricted domains for sign-ups', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :restricted_signup_domains_raw, placeholder: 'domain.com', class: 'form-control'
+ .help-block Only users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
+
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 39b66647a5a..1632dd8affa 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Settings"
%h3.page-title Application settings
%hr
= render 'form'
diff --git a/app/views/admin/applications/_delete_form.html.haml b/app/views/admin/applications/_delete_form.html.haml
index 371ac55209f..3147cbd659f 100644
--- a/app/views/admin/applications/_delete_form.html.haml
+++ b/app/views/admin/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
-- submit_btn_css ||= 'btn btn-link btn-remove btn-small'
+- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag admin_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file
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/applications/edit.html.haml b/app/views/admin/applications/edit.html.haml
index e408ae2f29d..c596866bde2 100644
--- a/app/views/admin/applications/edit.html.haml
+++ b/app/views/admin/applications/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", @application.name, "Applications"
%h3.page-title Edit application
- @url = admin_application_path(@application)
-= render 'form', application: @application \ No newline at end of file
+= render 'form', application: @application
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index d550278710e..fc921a966f3 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Applications"
%h3.page-title
System OAuth applications
%p.light
diff --git a/app/views/admin/applications/new.html.haml b/app/views/admin/applications/new.html.haml
index 7c62425f19c..6310d89bd6b 100644
--- a/app/views/admin/applications/new.html.haml
+++ b/app/views/admin/applications/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Application"
%h3.page-title New application
- @url = admin_applications_path
-= render 'form', application: @application \ No newline at end of file
+= render 'form', application: @application
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 2abe390ce13..0ea2ffeda99 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @application.name, "Applications"
%h3.page-title
Application: #{@application.name}
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 8db2b2a709c..3a01e115109 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Background Jobs"
%h3.page-title Background Jobs
%p.light GitLab uses #{link_to "sidekiq", "http://sidekiq.org/"} library for async job processing
@@ -41,4 +42,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/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 7b483ee6556..267c9a52921 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Broadcast Messages"
%h3.page-title
Broadcast Messages
%p.light
@@ -21,13 +22,11 @@
.form-group.js-toggle-colors-container.hide
= f.label :color, "Background Color", class: 'control-label'
.col-sm-10
- = f.text_field :color, placeholder: "#AA33EE", class: "form-control"
- .light 6 character hex values starting with a # sign.
+ = f.color_field :color, value: "#AA33EE", class: "form-control"
.form-group.js-toggle-colors-container.hide
= f.label :font, "Font Color", class: 'control-label'
.col-sm-10
- = f.text_field :font, placeholder: "#224466", class: "form-control"
- .light 6 character hex values starting with a # sign.
+ = f.color_field :font, value: "#224466", class: "form-control"
.form-group
= f.label :starts_at, class: 'control-label'
.col-sm-10.datetime-controls
@@ -52,7 +51,7 @@
%strong
#{broadcast_message.ends_at.to_s(:short)}
&nbsp;
- = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-tiny' do
+ = link_to [:admin, broadcast_message], method: :delete, remote: true, class: 'remove-row btn btn-xs' do
%i.fa.fa-times.cred
.message= broadcast_message.message
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..367d25cd6a1
--- /dev/null
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -0,0 +1,28 @@
+- page_title "Deploy Keys"
+.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..5b46b3222a9
--- /dev/null
+++ b/app/views/admin/deploy_keys/new.html.haml
@@ -0,0 +1,27 @@
+- page_title "New Deploy Key"
+%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..ea361ca4bdb
--- /dev/null
+++ b/app/views/admin/deploy_keys/show.html.haml
@@ -0,0 +1,35 @@
+- page_title @deploy_key.title, "Deploy Keys"
+.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/edit.html.haml b/app/views/admin/groups/edit.html.haml
index 824e51c1cf1..eb09a6328ed 100644
--- a/app/views/admin/groups/edit.html.haml
+++ b/app/views/admin/groups/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", @group.name, "Groups"
%h3.page-title Edit group: #{@group.name}
%hr
= render 'form'
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 8ae9a1edea9..e00b23ad99f 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Groups"
%h3.page-title
Groups (#{@groups.total_count})
= link_to 'New Group', new_admin_group_path, class: "btn btn-new pull-right"
@@ -40,8 +41,8 @@
%li
.clearfix
.pull-right.prepend-top-10
- = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-small"
- = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-small btn-remove"
+ = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: "btn btn-sm"
+ = link_to 'Destroy', [:admin, group], data: {confirm: "REMOVE #{group.name}? Are you sure?"}, method: :delete, class: "btn btn-sm btn-remove"
%h4
= link_to [:admin, group] do
diff --git a/app/views/admin/groups/new.html.haml b/app/views/admin/groups/new.html.haml
index f46f45c5514..c81ee552ac3 100644
--- a/app/views/admin/groups/new.html.haml
+++ b/app/views/admin/groups/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Group"
%h3.page-title New group
%hr
= render 'form'
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index bb7f1972925..187314872de 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @group.name, "Groups"
%h3.page-title
Group: #{@group.name}
@@ -12,7 +13,7 @@
Group info:
%ul.well-list
%li
- = image_tag group_icon(@group.path), class: "avatar s60"
+ = image_tag group_icon(@group), class: "avatar s60"
%li
%span.light Name:
%strong= @group.name
@@ -58,9 +59,9 @@
Read more about project permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
- = form_tag project_teams_update_admin_group_path(@group), id: "new_team_member", class: "bulk_import", method: :put do
+ = 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, scope: :all)
%div.prepend-top-10
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
@@ -74,13 +75,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-tiny 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/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index 0c5db0805f9..e74e1e85f41 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "System Hooks"
%h3.page-title
System hooks
@@ -33,5 +34,5 @@
%strong= hook.url
.pull-right
- = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-small"
- = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-small"
+ = link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm"
+ = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
diff --git a/app/views/admin/keys/show.html.haml b/app/views/admin/keys/show.html.haml
index 5b23027b3ab..9ee77c77398 100644
--- a/app/views/admin/keys/show.html.haml
+++ b/app/views/admin/keys/show.html.haml
@@ -1 +1,2 @@
+- page_title @key.title, "Keys"
= render "profiles/keys/key_details", admin: true
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 384c6ee9af5..1484baa78e0 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Logs"
- loggers = [Gitlab::GitLogger, Gitlab::AppLogger,
Gitlab::ProductionLogger, Gitlab::SidekiqLogger]
%ul.nav.nav-tabs.log-tabs
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 3a1e61d5d8d..f43d46356fa 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,6 +1,7 @@
+- page_title "Projects"
+= render 'shared/show_aside'
+
.row
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
%aside.col-md-3
.admin-filter
= form_tag admin_namespaces_projects_path, method: :get, class: '' do
@@ -74,8 +75,8 @@
.pull-right
%span.label.label-gray
= repository_size(project)
- = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small"
- = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-small btn-remove"
+ = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
+ = link_to 'Destroy', [project.namespace.becomes(Namespace), project], data: { confirm: remove_project_message(project) }, method: :delete, class: "btn btn-sm btn-remove"
- if @projects.blank?
.nothing-here-block 0 projects matches
= paginate @projects, theme: "gitlab"
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 1421c2ea909..4c2865ac3f2 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @project.name_with_namespace, "Projects"
%h3.page-title
Project: #{@project.name_with_namespace}
= link_to edit_project_path(@project), class: "btn pull-right" do
@@ -42,11 +43,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:
@@ -68,6 +69,11 @@
%strong.cred
does not exist
+ - if @project.archived?
+ %li
+ %span.light archived:
+ %strong repository is read-only
+
%li
%span.light access:
%strong
@@ -97,7 +103,7 @@
%strong #{@group.name}
group members (#{@group.group_members.count})
.pull-right
- = link_to admin_group_path(@group), class: 'btn btn-small' do
+ = link_to admin_group_path(@group), class: 'btn btn-xs' do
%i.fa.fa-pencil-square-o
%ul.well-list
- @group_members.each do |member|
@@ -111,22 +117,27 @@
%small
(#{@project.users.count})
.pull-right
- = link_to namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-tiny" do
+ = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do
%i.fa.fa-pencil-square-o
Manage Access
- %ul.well-list.team_members
+ %ul.well-list.project_members
- @project_members.each do |project_member|
- 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_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, remote: true, class: "btn btn-small 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/services/_form.html.haml b/app/views/admin/services/_form.html.haml
index 291e48efc12..cdbfc60f9a4 100644
--- a/app/views/admin/services/_form.html.haml
+++ b/app/views/admin/services/_form.html.haml
@@ -3,79 +3,8 @@
%p #{@service.description} template
-= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |f|
- - if @service.errors.any?
- #error_explanation
- .alert.alert-danger
- - @service.errors.full_messages.each do |msg|
- %p= msg
- - if @service.help.present?
- .alert.alert-info
- = preserve do
- = markdown @service.help
-
- .form-group
- = f.label :url, "Trigger", class: 'control-label'
- - if @service.supported_events.length > 1
- .col-sm-10
- - if @service.supported_events.include?("push")
- %div
- = f.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This url will be triggered by a push to the repository
- - if @service.supported_events.include?("tag_push")
- %div
- = f.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This url will be triggered when a new tag is pushed to the repository
- - if @service.supported_events.include?("issue")
- %div
- = f.check_box :issues_events, class: 'pull-left'
- .prepend-left-20
- = f.label :issues_events, class: 'list-label' do
- %strong Issues events
- %p.light
- This url will be triggered when an issue is created
- - if @service.supported_events.include?("merge_request")
- %div
- = f.check_box :merge_requests_events, class: 'pull-left'
- .prepend-left-20
- = f.label :merge_requests_events, class: 'list-label' do
- %strong Merge Request events
- %p.light
- This url will be triggered when a merge request is created
-
- - @service.fields.each do |field|
- - name = field[:name]
- - title = field[:title] || name.humanize
- - value = @service.send(name) unless field[:type] == 'password'
- - type = field[:type]
- - placeholder = field[:placeholder]
- - choices = field[:choices]
- - default_choice = field[:default_choice]
- - help = field[:help]
-
- .form-group
- = f.label name, title, class: "control-label"
- .col-sm-10
- - if type == 'text'
- = f.text_field name, class: "form-control", placeholder: placeholder
- - elsif type == 'textarea'
- = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
- - elsif type == 'checkbox'
- = f.check_box name
- - elsif type == 'select'
- = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- - elsif type == 'password'
- = f.password_field name, class: 'form-control'
- - if help
- %span.help-block= help
+= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'form-horizontal fieldset-form' } do |form|
+ = render 'shared/service_settings', form: form
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/services/edit.html.haml b/app/views/admin/services/edit.html.haml
index bcc5832792f..53d970e33c1 100644
--- a/app/views/admin/services/edit.html.haml
+++ b/app/views/admin/services/edit.html.haml
@@ -1 +1,2 @@
+- page_title @service.title, "Service Templates"
= render 'form'
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 0093fb97765..e2377291142 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Service Templates"
%h3.page-title Service templates
%p.light Service template allows you to set default values for project services
diff --git a/app/views/admin/users/edit.html.haml b/app/views/admin/users/edit.html.haml
index d71d8189c51..a8837d74dd9 100644
--- a/app/views/admin/users/edit.html.haml
+++ b/app/views/admin/users/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", @user.name, "Users"
%h3.page-title
Edit user: #{@user.name}
.back-link
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 35e9fd5154f..fe648470233 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -1,6 +1,7 @@
+- page_title "Users"
+= render 'shared/show_aside'
+
.row
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
%aside.col-md-3
.admin-filter
%ul.nav.nav-pills.nav-stacked
@@ -78,11 +79,11 @@
%i.fa.fa-envelope
= mail_to user.email, user.email, class: 'light'
&nbsp;
- = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-small"
+ = link_to 'Edit', edit_admin_user_path(user), id: "edit_#{dom_id(user)}", class: "btn btn-sm"
- unless user == current_user
- if user.blocked?
- = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-small success"
+ = link_to 'Unblock', unblock_admin_user_path(user), method: :put, class: "btn btn-sm success"
- else
- = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-small btn-remove"
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-small btn-remove"
+ = link_to 'Block', block_admin_user_path(user), data: {confirm: 'USER WILL BE BLOCKED! Are you sure?'}, method: :put, class: "btn btn-sm btn-remove"
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-sm btn-remove"
= paginate @users, theme: "gitlab"
diff --git a/app/views/admin/users/new.html.haml b/app/views/admin/users/new.html.haml
index 8fbb757f424..bfc36ed7373 100644
--- a/app/views/admin/users/new.html.haml
+++ b/app/views/admin/users/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New User"
%h3.page-title
New user
%hr
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index bae9a97bf36..7fc85206109 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @user.name, "Users"
%h3.page-title
User:
= @user.name
@@ -46,7 +47,7 @@
%li
%span.light Secondary email:
%strong= email.email
- = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-tiny btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
+ = link_to remove_email_admin_user_path(@user, email), data: { confirm: "Are you sure you want to remove #{email.email}?" }, method: :delete, class: "btn-xs btn btn-remove pull-right", title: 'Remove secondary email', id: "remove_email_#{email.id}" do
%i.fa.fa-times
%li
@@ -116,7 +117,6 @@
%ul
%li User will not be able to login
%li User will not be able to access git repositories
- %li User will be removed from joined projects and groups
%li Personal projects will be left
%li Owned groups will be left
%br
@@ -175,15 +175,15 @@
.panel.panel-default
.panel-heading Groups:
%ul.well-list
- - @user.group_members.each do |user_group|
- - group = user_group.group
+ - @user.group_members.each do |group_member|
+ - group = group_member.group
%li.group_member
- %span{class: ("list-item-name" unless user_group.owner?)}
+ %span{class: ("list-item-name" unless group_member.owner?)}
%strong= link_to group.name, admin_group_path(group)
.pull-right
- %span.light= user_group.human_access
- - unless user_group.owner?
- = link_to group_group_member_path(group, user_group), data: { confirm: remove_user_from_group_message(group, @user) }, method: :delete, remote: true, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
+ %span.light= group_member.human_access
+ - unless group_member.owner?
+ = link_to group_group_member_path(group, group_member), data: { confirm: remove_user_from_group_message(group, group_member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%i.fa.fa-times.fa-inverse
- else
.nothing-here-block This user has no groups.
@@ -208,21 +208,21 @@
.panel-heading Joined projects (#{@joined_projects.count})
%ul.well-list
- @joined_projects.sort_by(&:name_with_namespace).each do |project|
- - tm = project.team.find_tm(@user.id)
+ - member = project.team.find_member(@user.id)
%li.project_member
.list-item-name
= link_to admin_namespace_project_path(project.namespace, project), class: dom_class(project) do
= project.name_with_namespace
- - if tm
+ - if member
.pull-right
- - if tm.owner?
+ - if member.owner?
%span.light Owner
- else
- %span.light= tm.human_access
+ %span.light= member.human_access
- - if tm.respond_to? :project
- = link_to namespace_project_team_member_path(project.namespace, project, @user), data: { confirm: remove_from_project_team_message(project, @user) }, remote: true, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from project' do
+ - if member.respond_to? :project
+ = link_to namespace_project_project_member_path(project.namespace, project, member), data: { confirm: remove_from_project_team_message(project, member) }, remote: true, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from project' do
%i.fa.fa-times
#ssh-keys.tab-pane
= render 'profiles/keys/key_table', admin: true
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index c1fc1602d0a..ba49013d834 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,4 +1,14 @@
-= render "events/event_last_push", event: @last_push
-= render 'shared/event_filter'
+.hidden-xs
+ = render "events/event_last_push", event: @last_push
+
+ - if current_user
+ %ul.nav.nav-pills.event_filter.pull-right
+ %li.pull-right
+ = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do
+ %i.fa.fa-rss
+ Activity Feed
+
+ = render 'shared/event_filter'
+ %hr
.content_list
= spinner
diff --git a/app/views/dashboard/_groups.html.haml b/app/views/dashboard/_groups.html.haml
deleted file mode 100644
index e3df43d8892..00000000000
--- a/app/views/dashboard/_groups.html.haml
+++ /dev/null
@@ -1,21 +0,0 @@
-.panel.panel-default
- .panel-heading.clearfix
- .input-group
- = search_field_tag :filter_group, nil, placeholder: 'Filter by name', class: 'dash-filter form-control'
- - if current_user.can_create_group?
- .input-group-addon.dash-new-group
- = link_to new_group_path, class: "" do
- %strong New group
- %ul.well-list.dash-list
- - groups.each do |group|
- %li.group-row
- = link_to group_path(id: group.path), class: dom_class(group) do
- .dash-project-avatar
- = image_tag group_icon(group.path), class: "avatar s40"
- %span.group-name.filter-title
- = truncate(group.name, length: 35)
- %span.arrow
- %i.fa.fa-angle-right
- - if groups.blank?
- %li
- .nothing-here-block You have no groups yet.
diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/_projects.html.haml
index 3634b2bfd7b..d676576067c 100644
--- a/app/views/dashboard/_projects.html.haml
+++ b/app/views/dashboard/_projects.html.haml
@@ -3,8 +3,8 @@
.input-group
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
- if current_user.can_create_project?
- .input-group-addon.dash-new-project
- = link_to new_project_path do
- %strong New project
+ %span.input-group-btn
+ = link_to new_project_path, class: 'btn btn-success' do
+ New project
= render 'shared/projects_list', projects: @projects, projects_limit: 20
diff --git a/app/views/dashboard/_projects_filter.html.haml b/app/views/dashboard/_projects_filter.html.haml
deleted file mode 100644
index d87ca861aed..00000000000
--- a/app/views/dashboard/_projects_filter.html.haml
+++ /dev/null
@@ -1,100 +0,0 @@
-.dash-projects-filters.append-bottom-20
- .append-right-20
- %ul.nav.nav-tabs
- = nav_tab :scope, nil do
- = link_to projects_dashboard_filter_path(scope: nil) do
- All
- = nav_tab :scope, 'personal' do
- = link_to projects_dashboard_filter_path(scope: 'personal') do
- Personal
- = nav_tab :scope, 'joined' do
- = link_to projects_dashboard_filter_path(scope: 'joined') do
- Joined
- = nav_tab :scope, 'owned' do
- = link_to projects_dashboard_filter_path(scope: 'owned') do
- Owned
-
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-globe
- %span.light Visibility:
- - if params[:visibility_level].present?
- = visibility_level_label(params[:visibility_level].to_i)
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to projects_dashboard_filter_path(visibility_level: nil) do
- Any
- - Gitlab::VisibilityLevel.values.each do |level|
- %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
- = link_to projects_dashboard_filter_path(visibility_level: level) do
- = visibility_level_icon(level)
- = visibility_level_label(level)
-
- - if @groups.present?
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-group
- %span.light Group:
- - if params[:group].present?
- = Group.find_by(name: params[:group]).name
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to projects_dashboard_filter_path(group: nil) do
- Any
- - @groups.each do |group|
- %li{ class: (group.name == params[:group]) ? 'active' : 'light' }
- = link_to projects_dashboard_filter_path(group: group.name) do
- = group.name
- %small.pull-right
- = group.projects.count
-
-
-
- - if @tags.present?
- .dropdown.inline.append-right-10
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %i.fa.fa-tags
- %span.light Tags:
- - if params[:tag].present?
- = params[:tag]
- - else
- Any
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to projects_dashboard_filter_path(tag: nil) do
- Any
-
- - @tags.each do |tag|
- %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
- = link_to projects_dashboard_filter_path(tag: tag.name) do
- %i.fa.fa-tag
- = tag.name
-
- .pull-right
- .dropdown.inline
- %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to projects_dashboard_filter_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to projects_dashboard_filter_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to projects_dashboard_filter_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to projects_dashboard_filter_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
- = link_to projects_dashboard_filter_path(sort: sort_value_name) do
- = sort_title_name
diff --git a/app/views/dashboard/_sidebar.html.haml b/app/views/dashboard/_sidebar.html.haml
index 983da4aba04..78f695be916 100644
--- a/app/views/dashboard/_sidebar.html.haml
+++ b/app/views/dashboard/_sidebar.html.haml
@@ -1,18 +1,3 @@
-%ul.nav.nav-tabs.dash-sidebar-tabs
- %li.active
- = link_to '#projects', 'data-toggle' => 'tab', id: 'sidebar-projects-tab' do
- Projects
- %span.badge= @projects_count
- %li
- = link_to '#groups', 'data-toggle' => 'tab', id: 'sidebar-groups-tab' do
- Groups
- %span.badge= @groups.count
-
-.tab-content
- .tab-pane.active#projects
- = render "dashboard/projects", projects: @projects
- .tab-pane#groups
- = render "dashboard/groups", groups: @groups
-
+= render "dashboard/projects", projects: @projects
.prepend-top-20
= render 'shared/promo'
diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/_zero_authorized_projects.html.haml
index 6e76f95b34e..4e7d6639727 100644
--- a/app/views/dashboard/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/_zero_authorized_projects.html.haml
@@ -1,3 +1,4 @@
+- publicish_project_count = Project.publicish(current_user).count
%h3.page-title Welcome to GitLab!
%p.light Self hosted Git management application.
%hr
@@ -35,7 +36,7 @@
%i.fa.fa-plus
New Group
--if @publicish_project_count > 0
+-if publicish_project_count > 0
%hr
%div
.dashboard-intro-icon
@@ -43,7 +44,7 @@
.dashboard-intro-text
%p.slead
There are
- %strong= @publicish_project_count
+ %strong= publicish_project_count
public projects on this server.
%br
Public projects are an easy way to allow everyone to have read-only access.
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index fd7bbb5500c..5ecd53cff84 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Groups"
%h3.page-title
Group Membership
- if current_user.can_create_group?
@@ -11,29 +12,30 @@
.panel.panel-default
.panel-heading
%strong Groups
- (#{@user_groups.count})
+ (#{@group_members.count})
%ul.well-list
- - @user_groups.each do |user_group|
- - group = user_group.group
+ - @group_members.each do |group_member|
+ - group = group_member.group
%li
.pull-right
- - if can?(current_user, :manage_group, group)
- = link_to edit_group_path(group), class: "btn-small btn btn-grouped" do
+ - 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
- - if can?(current_user, :destroy, user_group)
- = link_to leave_dashboard_group_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-small btn btn-grouped", title: 'Remove user from group' do
+ - if can?(current_user, :destroy_group_member, group_member)
+ = link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Remove user from group' do
%i.fa.fa-sign-out
Leave
+ = image_tag group_icon(group), class: "avatar s40 avatar-tile"
= link_to group, class: 'group-name' do
%strong= group.name
as
- %strong #{user_group.human_access}
+ %strong #{group_member.human_access}
%div.light
#{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")}
-= paginate @user_groups
+= paginate @group_members
diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder
index 72e9e361dc3..6e88fc9be40 100644
--- a/app/views/dashboard/issues.atom.builder
+++ b/app/views/dashboard/issues.atom.builder
@@ -1,9 +1,9 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{current_user.name} issues"
- xml.link href: issues_dashboard_url(:atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
- xml.link href: issues_dashboard_url(private_token: current_user.private_token), rel: "alternate", type: "text/html"
- xml.id issues_dashboard_url(private_token: current_user.private_token)
+ xml.link href: issues_dashboard_url(format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
+ xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
+ xml.id issues_dashboard_url
xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
@issues.each do |issue|
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index db19a46cb26..dfdf0d68c8f 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,3 +1,8 @@
+- page_title "Issues"
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
+
%h3.page-title
Issues
@@ -6,5 +11,11 @@
%hr
.append-bottom-20
+ .pull-right
+ - if current_user
+ .hidden-xs.pull-left
+ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
+ %i.fa.fa-rss
+
= render 'shared/issuable_filter'
= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 97a42461b4e..a7e1b08a0a4 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,3 +1,4 @@
+- page_title "Merge Requests"
%h3.page-title
Merge Requests
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..9a9a5e139a4 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Milestones"
%h3.page-title
Milestones
%span.pull-right #{@dashboard_milestones.count} milestones
@@ -16,23 +17,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/milestones/show.html.haml b/app/views/dashboard/milestones/show.html.haml
index 57cce9ab749..24f0bcb60d5 100644
--- a/app/views/dashboard/milestones/show.html.haml
+++ b/app/views/dashboard/milestones/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @dashboard_milestone.title, "Milestones"
%h4.page-title
.issue-box{ class: "issue-box-#{@dashboard_milestone.closed? ? 'closed' : 'open'}" }
- if @dashboard_milestone.closed?
diff --git a/app/views/dashboard/projects.html.haml b/app/views/dashboard/projects.html.haml
deleted file mode 100644
index 03d4b3d8bbb..00000000000
--- a/app/views/dashboard/projects.html.haml
+++ /dev/null
@@ -1,60 +0,0 @@
-%h3.page-title
- My Projects
-
- = link_to new_project_path, class: "btn btn-new pull-right" do
- %i.fa.fa-plus
- New Project
-
-%p.light
- All projects you have access to are listed here. Public projects are not included here unless you are a member
-%hr
-.side-filters
- = render "projects_filter"
-.dash-projects
- %ul.bordered-list.my-projects.top-list
- - @projects.each do |project|
- %li.my-project-row
- %h4.project-title
- .pull-left
- = project_icon(project, alt: '', class: 'avatar project-avatar s60')
- .project-access-icon
- = visibility_level_icon(project.visibility_level)
- = link_to project_path(project), class: dom_class(project) do
- %strong= project.name_with_namespace
-
- - if project.forked_from_project
- &nbsp;
- %small
- %i.fa.fa-code-fork
- Forked from:
- = link_to project.forked_from_project.name_with_namespace, namespace_project_path(project.namespace, project.forked_from_project)
-
- - if current_user.can_leave_project?(project)
- .pull-right
- = link_to leave_namespace_project_team_members_path(project.namespace, project), data: { confirm: "Leave project?"}, method: :delete, remote: true, class: "btn-tiny btn remove-row", title: 'Leave project' do
- %i.fa.fa-sign-out
- Leave
-
- .project-info
- .pull-right
- - if project.archived?
- %span.label
- %i.fa.fa-archive
- Archived
- - project.tags.each do |tag|
- %span.label.label-info
- %i.fa.fa-tag
- = tag.name
- - if project.description.present?
- %p= truncate project.description, length: 100
- .last-activity
- %span.light Last activity:
- %span.date= project_last_activity(project)
-
-
- - if @projects.blank?
- %li
- .nothing-here-block There are no projects here.
- .bottom
- = paginate @projects, theme: "gitlab"
-
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 94de6092563..8aaa0a7f071 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,4 +1,7 @@
+- page_title "Starred Projects"
- if @projects.any?
+ = render 'shared/show_aside'
+
.dashboard.row
%section.activities.col-md-8
= render 'dashboard/activities'
@@ -8,16 +11,13 @@
.input-group
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
- if current_user.can_create_project?
- .input-group-addon.dash-new-project
- = link_to new_project_path do
- %strong New project
+ %span.input-group-btn
+ = link_to new_project_path, class: 'btn btn-success' do
+ New project
= render 'shared/projects_list', projects: @projects,
projects_limit: 20, stars: true, avatar: false
- = link_to '#aside', class: 'show-aside' do
- %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/dashboard/show.atom.builder b/app/views/dashboard/show.atom.builder
index da631ecb33e..71edb73cd8a 100644
--- a/app/views/dashboard/show.atom.builder
+++ b/app/views/dashboard/show.atom.builder
@@ -1,9 +1,9 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "Dashboard feed#{" - #{current_user.name}" if current_user.name.present?}"
- xml.link href: dashboard_url(:atom), rel: "self", type: "application/atom+xml"
+ xml.title "Activity"
+ xml.link href: dashboard_url(format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
xml.link href: dashboard_url, rel: "alternate", type: "text/html"
- xml.id projects_url
+ xml.id dashboard_url
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml
index f973f4829a0..5001c2101e1 100644
--- a/app/views/dashboard/show.html.haml
+++ b/app/views/dashboard/show.html.haml
@@ -1,11 +1,15 @@
-- if @has_authorized_projects
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+
+- if @projects.any?
+ = render 'shared/show_aside'
+
.dashboard.row
%section.activities.col-md-8
= render 'activities'
%aside.col-md-4
= render 'sidebar'
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
- else
= render "zero_authorized_projects"
diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb
index cb1291cf3bf..c6fa8f0ee36 100644
--- a/app/views/devise/mailer/confirmation_instructions.html.erb
+++ b/app/views/devise/mailer/confirmation_instructions.html.erb
@@ -6,4 +6,4 @@
<p>You can confirm your account through the link below:</p>
<% end %>
-<p><%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %></p>
+<p><%= link_to 'Confirm your account', confirmation_url(@resource, confirmation_token: @token) %></p>
diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb
index 7913e88beb6..23b31da92d8 100644
--- a/app/views/devise/mailer/reset_password_instructions.html.erb
+++ b/app/views/devise/mailer/reset_password_instructions.html.erb
@@ -2,7 +2,7 @@
<p>Someone has requested a link to change your password, and you can do this through the link below.</p>
-<p><%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %></p>
+<p><%= link_to 'Change your password', edit_password_url(@resource, reset_password_token: @token) %></p>
<p>If you didn't request this, please ignore this email.</p>
<p>Your password won't change until you access the link above and create a new one.</p>
diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb
index 8c2a4f0c2d9..79d6c761d8f 100644
--- a/app/views/devise/mailer/unlock_instructions.html.erb
+++ b/app/views/devise/mailer/unlock_instructions.html.erb
@@ -4,4 +4,4 @@
<p>Click the link below to unlock your account:</p>
-<p><%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %></p>
+<p><%= link_to 'Unlock your account', unlock_url(@resource, unlock_token: @token) %></p>
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 0640739b5d7..56048e99c17 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -11,7 +11,7 @@
%div
= f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true
.clearfix
- = f.submit "Change my password", class: "btn btn-primary"
+ = f.submit "Change your password", class: "btn btn-primary"
.clearfix.prepend-top-20
%p
diff --git a/app/views/devise/registrations/edit.html.erb b/app/views/devise/registrations/edit.html.erb
index b11817af95d..f379e71ae5b 100644
--- a/app/views/devise/registrations/edit.html.erb
+++ b/app/views/devise/registrations/edit.html.erb
@@ -21,8 +21,8 @@
<div><%= f.submit "Update", class: "input_button" %></div>
<% end %>
-<h3>Cancel my account</h3>
+<h3>Cancel your account</h3>
-<p>Unhappy? <%= link_to "Cancel my account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.</p>
+<p>Unhappy? <%= link_to "Cancel your account", registration_path(resource_name), data: { confirm: "Are you sure?" }, method: :delete %>.</p>
<%= link_to "Back", :back %>
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index d3e37f7494c..42cfbbf84f2 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "Sign up"
= render 'devise/shared/signup_box'
-= render 'devise/shared/sign_in_link' \ No newline at end of file
+= render 'devise/shared/sign_in_link'
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index e986989a728..812e22373a7 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,4 +1,4 @@
-= form_tag(user_omniauth_callback_path(provider), id: 'new_ldap_user' ) do
- = text_field_tag :username, nil, {class: "form-control top", placeholder: "LDAP Login", autofocus: "autofocus"}
+= form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do
+ = text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- = button_tag "LDAP Sign in", class: "btn-save btn"
+ = button_tag "#{server['label']} Sign in", class: "btn-save btn"
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 89e4e229ac0..dbc8eda6196 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "Sign in"
%div
- if signin_enabled? || ldap_enabled?
= render 'devise/shared/signin_box'
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 4cd1c303b22..8dce0b16936 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -5,6 +5,6 @@
- providers.each do |provider|
%span.light
- if default_providers.include?(provider)
- = link_to authbutton(provider, 32), omniauth_authorize_path(resource_name, provider)
+ = link_to oauth_image_tag(provider), omniauth_authorize_path(resource_name, provider), class: 'oauth-image-link'
- else
- = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn"
+ = link_to provider.to_s.titleize, omniauth_authorize_path(resource_name, provider), class: "btn", "data-no-turbolink" => "true"
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index 8faa6398a60..c76574db457 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -17,7 +17,7 @@
.tab-content
- @ldap_servers.each_with_index do |server, i|
%div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)}
- = render 'devise/sessions/new_ldap', provider: server['provider_name']
+ = render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
%div#tab-signin.tab-pane
= render 'devise/sessions/new_base'
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/doorkeeper/applications/_delete_form.html.haml b/app/views/doorkeeper/applications/_delete_form.html.haml
index bf8098f38d0..6a5c917049d 100644
--- a/app/views/doorkeeper/applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
-- submit_btn_css ||= 'btn btn-link btn-remove btn-small'
+- submit_btn_css ||= 'btn btn-link btn-remove btn-sm'
= form_tag oauth_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
= submit_tag 'Destroy', onclick: "return confirm('Are you sure?')", class: submit_btn_css \ No newline at end of file
diff --git a/app/views/doorkeeper/applications/edit.html.haml b/app/views/doorkeeper/applications/edit.html.haml
index 61584eb9c49..fb6aa30acee 100644
--- a/app/views/doorkeeper/applications/edit.html.haml
+++ b/app/views/doorkeeper/applications/edit.html.haml
@@ -1,2 +1,3 @@
+- page_title "Edit", @application.name, "Applications"
%h3.page-title Edit application
-= render 'form', application: @application \ No newline at end of file
+= render 'form', application: @application
diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml
index e5be4b4bcac..3b0b19107ca 100644
--- a/app/views/doorkeeper/applications/index.html.haml
+++ b/app/views/doorkeeper/applications/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Applications"
%h3.page-title Your applications
%p= link_to 'New Application', new_oauth_application_path, class: 'btn btn-success'
%table.table.table-striped
@@ -13,4 +14,4 @@
%td= link_to application.name, oauth_application_path(application)
%td= application.redirect_uri
%td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link'
- %td= render 'delete_form', application: application \ No newline at end of file
+ %td= render 'delete_form', application: application
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 82e78b4af13..80340aca54c 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @application.name, "Applications"
%h3.page-title
Application: #{@application.name}
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index 5cbb4a70c19..4bba72167e3 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -1,4 +1,4 @@
- submit_btn_css ||= 'btn btn-link btn-remove'
= form_tag oauth_authorized_application_path(application) do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-small' \ No newline at end of file
+ = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm' \ No newline at end of file
diff --git a/app/views/errors/access_denied.html.haml b/app/views/errors/access_denied.html.haml
index a1d8664c4ce..012e9857642 100644
--- a/app/views/errors/access_denied.html.haml
+++ b/app/views/errors/access_denied.html.haml
@@ -1,3 +1,4 @@
+- page_title "Access Denied"
%h1 403
%h3 Access Denied
%hr
diff --git a/app/views/errors/encoding.html.haml b/app/views/errors/encoding.html.haml
index 64c7451a8da..90cfbebfcc6 100644
--- a/app/views/errors/encoding.html.haml
+++ b/app/views/errors/encoding.html.haml
@@ -1,3 +1,4 @@
+- page_title "Encoding Error"
%h1 500
%h3 Encoding Error
%hr
diff --git a/app/views/errors/git_not_found.html.haml b/app/views/errors/git_not_found.html.haml
index 189e53bca55..ff5d4cc1506 100644
--- a/app/views/errors/git_not_found.html.haml
+++ b/app/views/errors/git_not_found.html.haml
@@ -1,3 +1,4 @@
+- page_title "Git Resource Not Found"
%h1 404
%h3 Git Resource Not found
%hr
diff --git a/app/views/errors/not_found.html.haml b/app/views/errors/not_found.html.haml
index 7bf88f592cf..3756b98ebb2 100644
--- a/app/views/errors/not_found.html.haml
+++ b/app/views/errors/not_found.html.haml
@@ -1,3 +1,4 @@
+- page_title "Not Found"
%h1 404
%h3 The resource you were looking for doesn't exist.
%hr
diff --git a/app/views/errors/omniauth_error.html.haml b/app/views/errors/omniauth_error.html.haml
index f3c8221a9d9..3e70e98a24c 100644
--- a/app/views/errors/omniauth_error.html.haml
+++ b/app/views/errors/omniauth_error.html.haml
@@ -1,3 +1,4 @@
+- page_title "Auth Error"
%h1 422
%h3 Sign-in using #{@provider} auth failed
%hr
diff --git a/app/views/events/_event_issue.atom.haml b/app/views/events/_event_issue.atom.haml
index eba2b63797a..0edb61ea246 100644
--- a/app/views/events/_event_issue.atom.haml
+++ b/app/views/events/_event_issue.atom.haml
@@ -1,3 +1,3 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- if issue.description.present?
- = markdown issue.description
+ = markdown(issue.description, xhtml: true)
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index cb40aa9970b..d2f0005142a 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -9,6 +9,6 @@
#{time_ago_with_tooltip(event.created_at)}
.pull-right
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-small" do
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-create btn-sm" do
Create Merge Request
%hr
diff --git a/app/views/events/_event_merge_request.atom.haml b/app/views/events/_event_merge_request.atom.haml
index 0aea2d17d65..1a8b62abeab 100644
--- a/app/views/events/_event_merge_request.atom.haml
+++ b/app/views/events/_event_merge_request.atom.haml
@@ -1,3 +1,3 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- if merge_request.description.present?
- = markdown merge_request.description
+ = markdown(merge_request.description, xhtml: true)
diff --git a/app/views/events/_event_note.atom.haml b/app/views/events/_event_note.atom.haml
index be0e05481ed..b49c331ccf2 100644
--- a/app/views/events/_event_note.atom.haml
+++ b/app/views/events/_event_note.atom.haml
@@ -1,2 +1,2 @@
%div{xmlns: "http://www.w3.org/1999/xhtml"}
- = markdown note.note
+ = markdown(note.note, xhtml: true)
diff --git a/app/views/events/_event_push.atom.haml b/app/views/events/_event_push.atom.haml
index 0ffd2aa0b98..5d14def8f75 100644
--- a/app/views/events/_event_push.atom.haml
+++ b/app/views/events/_event_push.atom.haml
@@ -6,7 +6,7 @@
%i
at
= commit[:timestamp].to_time.to_s(:short)
- %blockquote= markdown(escape_once(commit[:message]))
+ %blockquote= markdown(escape_once(commit[:message]), xhtml: true)
- if event.commits_count > 15
%p
%i
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index 3c7153d235f..c2577a24982 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.action_name} a new project on 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/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 489138887ae..60d7978b13f 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -21,5 +21,11 @@
%li.commits-stat
- if event.commits_count > 2
%span ... and #{event.commits_count - 2} more commits.
- = link_to namespace_project_compare_path(event.project.namespace, event.project, from: event.commit_from, to: event.commit_to) do
- %strong Compare &rarr; #{truncate_sha(event.commit_from)}...#{truncate_sha(event.commit_to)}
+ - if event.md_ref?
+ - from = event.commit_from
+ - from_label = truncate_sha(from)
+ - else
+ - from = event.project.default_branch
+ - from_label = from
+ = link_to namespace_project_compare_path(event.project.namespace, event.project, from: from, to: event.commit_to) do
+ %strong Compare &rarr; #{from_label}...#{truncate_sha(event.commit_to)}
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 2ea6cb18655..c05d45e0100 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Groups"
.clearfix
.pull-left
= form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f|
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
new file mode 100644
index 00000000000..b3963a9d901
--- /dev/null
+++ b/app/views/explore/projects/_filter.html.haml
@@ -0,0 +1,67 @@
+.pull-left
+ = form_tag explore_projects_filter_path, method: :get, class: 'form-inline form-tiny' do |f|
+ .form-group
+ = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search"
+ .form-group
+ = button_tag 'Search', class: "btn btn-primary wide"
+
+.pull-right.hidden-sm.hidden-xs
+ - if current_user
+ .dropdown.inline.append-right-10
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %i.fa.fa-globe
+ %span.light Visibility:
+ - if params[:visibility_level].present?
+ = visibility_level_label(params[:visibility_level].to_i)
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to explore_projects_filter_path(visibility_level: nil) do
+ Any
+ - Gitlab::VisibilityLevel.values.each do |level|
+ %li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' }
+ = link_to explore_projects_filter_path(visibility_level: level) do
+ = visibility_level_icon(level)
+ = visibility_level_label(level)
+
+ - if @tags.present?
+ .dropdown.inline.append-right-10
+ %a.dropdown-toggle.btn{href: '#', "data-toggle" => "dropdown"}
+ %i.fa.fa-tags
+ %span.light Tags:
+ - if params[:tag].present?
+ = params[:tag]
+ - else
+ Any
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to explore_projects_filter_path(tag: nil) do
+ Any
+
+ - @tags.each do |tag|
+ %li{ class: (tag.name == params[:tag]) ? 'active' : 'light' }
+ = link_to explore_projects_filter_path(tag: tag.name) do
+ %i.fa.fa-tag
+ = tag.name
+
+ .dropdown.inline
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light sort:
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ %b.caret
+ %ul.dropdown-menu
+ %li
+ = link_to explore_projects_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to explore_projects_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+ = link_to explore_projects_filter_path(sort: sort_value_recently_updated) do
+ = sort_title_recently_updated
+ = link_to explore_projects_filter_path(sort: sort_value_oldest_updated) do
+ = sort_title_oldest_updated
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index cb93b300d6a..ba2276f51ce 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -1,30 +1,6 @@
+- page_title "Projects"
.clearfix
- .pull-left
- = form_tag explore_projects_path, method: :get, class: 'form-inline form-tiny' do |f|
- .form-group
- = search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input input-mn-300", id: "projects_search"
- .form-group
- = button_tag 'Search', class: "btn btn-primary wide"
-
- .pull-right
- .dropdown.inline
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light sort:
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- %b.caret
- %ul.dropdown-menu
- %li
- = link_to explore_projects_path(sort: sort_value_recently_created) do
- = sort_title_recently_created
- = link_to explore_projects_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
- = link_to explore_projects_path(sort: sort_value_recently_updated) do
- = sort_title_recently_updated
- = link_to explore_projects_path(sort: sort_value_oldest_updated) do
- = sort_title_oldest_updated
+ = render 'filter'
%hr
.public-projects
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 420f0693756..b5d146b1f2f 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -1,3 +1,4 @@
+- page_title "Starred Projects"
.explore-trending-block
%p.lead
%i.fa.fa-star
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 9cad9238933..5ae2653fede 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -1,3 +1,10 @@
+- page_title "Trending Projects"
+.explore-title
+ %h3
+ Explore GitLab
+ %p.lead
+ Discover projects and groups. Share your projects with others
+%hr
.explore-trending-block
%p.lead
%i.fa.fa-comments-o
@@ -7,5 +14,5 @@
%ul.bordered-list
= render @trending_projects
- .center
+ .center.append-bottom-20
= link_to 'Show all projects', explore_projects_path, class: 'btn btn-primary'
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index 6f53e125c47..4f8aec1c67e 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -3,8 +3,8 @@
.input-group
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
- if can? current_user, :create_projects, @group
- .input-group-addon.dash-new-project
- = link_to new_project_path(namespace_id: @group.id) do
- %strong New project
+ %span.input-group-btn
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-success' do
+ New project
= render 'shared/projects_list', projects: @projects, projects_limit: 20
diff --git a/app/views/groups/_settings_nav.html.haml b/app/views/groups/_settings_nav.html.haml
deleted file mode 100644
index e6aee22e529..00000000000
--- a/app/views/groups/_settings_nav.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-%ul.sidebar-subnav
- = nav_link(path: 'groups#edit') do
- = link_to edit_group_path(@group), title: 'Group' do
- %i.fa.fa-pencil-square-o
- %span
- Group
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects' do
- %i.fa.fa-folder
- %span
- Projects
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index c4eb00e8925..85179d4c4a2 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Settings"
.panel.panel-default
.panel-heading
%strong= @group.name
@@ -12,7 +13,7 @@
.form-group
.col-sm-2
.col-sm-10
- = image_tag group_icon(@group.to_param), alt: '', class: 'avatar group-avatar s160'
+ = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
%p.light
- if @group.avatar?
You can change your group avatar here
@@ -21,7 +22,7 @@
= render 'shared/choose_group_avatar_button', f: f
- if @group.avatar?
%hr
- = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar"
+ = link_to 'Remove avatar', group_avatar_path(@group.to_param), data: { confirm: "Group avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/groups/group_members/_group_member.html.haml b/app/views/groups/group_members/_group_member.html.haml
index 30c3c2b00df..56b1948a474 100644
--- a/app/views/groups/group_members/_group_member.html.haml
+++ b/app/views/groups/group_members/_group_member.html.haml
@@ -1,32 +1,54 @@
- 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 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
%strong= member.human_access
- if show_controls
- - if can?(current_user, :modify, member)
- = button_tag class: "btn-tiny btn js-toggle-button",
+ - if can?(current_user, :modify_group_member, member)
+ = button_tag class: "btn-xs btn js-toggle-button",
title: 'Edit access level', type: 'button' do
%i.fa.fa-pencil-square-o
- - if can?(current_user, :destroy, member)
- - if current_user == member.user
- = link_to leave_dashboard_group_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from group' do
+ - if can?(current_user, :destroy_group_member, member)
+ &nbsp;
+ - if current_user == user
+ = link_to leave_group_group_members_path(@group), data: { confirm: leave_group_message(@group.name)}, method: :delete, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
%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-tiny 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
+ %br
= form_for [@group, member], remote: true do |f|
- .alert.prepend-top-20
- = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level)
- = f.submit 'Save', class: 'btn btn-save btn-small'
+ .prepend-top-10
+ = f.select :access_level, options_for_select(GroupMember.access_level_roles, member.access_level), {}, class: 'form-control'
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/groups/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 345c0555a36..3361d7e2a8d 100644
--- a/app/views/groups/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -1,12 +1,15 @@
-= form_for @users_group, url: group_group_members_path(@group), html: { class: 'form-horizontal users-group-form' } do |f|
+= 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'
.col-sm-10
- = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @users_group.access_level), class: "project-access-select select2"
+ = select_tag :access_level, options_for_select(GroupMember.access_level_roles, @group_member.access_level), class: "project-access-select select2"
.help-block
Read more about role permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
diff --git a/app/views/groups/members.html.haml b/app/views/groups/group_members/index.html.haml
index 688c22e9624..903ca877218 100644
--- a/app/views/groups/members.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,4 +1,6 @@
+- page_title "Members"
- show_roles = should_user_see_group_roles?(current_user, @group)
+
%h3.page-title
Group members
- if show_roles
@@ -10,12 +12,12 @@
%hr
.clearfix.js-toggle-container
- = form_tag members_group_path(@group), method: :get, class: 'form-inline member-search-form' do
+ = form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
= search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input 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
@@ -33,6 +35,7 @@
%ul.well-list
- @members.each do |member|
= render 'groups/group_members/group_member', member: member, show_roles: show_roles, show_controls: true
+
= paginate @members, theme: 'gitlab'
:coffeescript
diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder
index 240001967f3..66fe7e25871 100644
--- a/app/views/groups/issues.atom.builder
+++ b/app/views/groups/issues.atom.builder
@@ -1,9 +1,9 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@user.name} issues"
- xml.link :href => issues_dashboard_url(:atom, :private_token => @user.private_token), :rel => "self", :type => "application/atom+xml"
- xml.link :href => issues_dashboard_url(:private_token => @user.private_token), :rel => "alternate", :type => "text/html"
- xml.id issues_dashboard_url(:private_token => @user.private_token)
+ xml.link href: issues_dashboard_url(format: :atom, private_token: @user.private_token), rel: "self", type: "application/atom+xml"
+ xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html"
+ xml.id issues_dashboard_url
xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
@issues.each do |issue|
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 6c0d89c4e7c..6a3da6adacf 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,3 +1,8 @@
+- page_title "Issues"
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
+
%h3.page-title
Issues
@@ -10,5 +15,11 @@
%hr
.append-bottom-20
+ .pull-right
+ - if current_user
+ .hidden-xs.pull-left
+ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
+ %i.fa.fa-rss
+
= render 'shared/issuable_filter'
= render 'shared/issues'
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 1ad74905636..268f33d5761 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,3 +1,4 @@
+- page_title "Merge Requests"
%h3.page-title
Merge Requests
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 9febaab04a7..385222fa5b7 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Milestones"
%h3.page-title
Milestones
%span.pull-right #{@group_milestones.count} milestones
@@ -18,29 +19,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-small 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-small 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 dd2d84499ba..6c41cd6b9e4 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @group_milestone.title, "Milestones"
%h4.page-title
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
- if @group_milestone.closed?
@@ -6,11 +7,11 @@
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-small btn-close"
+ = 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
- = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-small btn-grouped btn-reopen"
+ = link_to 'Reopen Milestone', group_milestone_path(@group, @group_milestone.safe_title, title: @group_milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
%hr
- if (@group_milestone.total_items_count == @group_milestone.closed_items_count) && @group_milestone.active?
diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml
index 6e17cdaef6f..edb882bea19 100644
--- a/app/views/groups/new.html.haml
+++ b/app/views/groups/new.html.haml
@@ -1,3 +1,5 @@
+- page_title 'New Group'
+- header_title 'New Group'
= form_for @group, html: { class: 'group-form form-horizontal' } do |f|
- if @group.errors.any?
.alert.alert-danger
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index c95347b3a55..6b7efa83dea 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,8 +1,9 @@
+- page_title "Projects"
.panel.panel-default
.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
@@ -16,9 +17,9 @@
%span.label.label-gray
= repository_size(project)
.pull-right
- = link_to 'Members', namespace_project_team_index_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small"
- = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-small"
- = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-small btn-remove"
+ = link_to 'Members', namespace_project_project_members_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
+ = link_to 'Edit', edit_namespace_project_path(project.namespace, project), id: "edit_#{dom_id(project)}", class: "btn btn-sm"
+ = link_to 'Remove', project, data: { confirm: remove_project_message(project)}, method: :delete, class: "btn btn-sm btn-remove"
- if @projects.blank?
.nothing-here-block This group has no projects yet
diff --git a/app/views/groups/show.atom.builder b/app/views/groups/show.atom.builder
index c78bd1bd263..b52e78faaa3 100644
--- a/app/views/groups/show.atom.builder
+++ b/app/views/groups/show.atom.builder
@@ -1,9 +1,9 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "Group feed - #{@group.name}"
- xml.link href: group_path(@group, :atom), rel: "self", type: "application/atom+xml"
- xml.link href: group_path(@group), rel: "alternate", type: "text/html"
- xml.id projects_url
+ xml.title "#{@group.name} activity"
+ xml.link href: group_url(@group, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
+ xml.link href: group_url(@group), rel: "alternate", type: "text/html"
+ xml.id group_url(@group)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index a453889f744..1678311141e 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,21 +1,38 @@
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
+
.dashboard
- %div
- = image_tag group_icon(@group.path), class: "avatar group-avatar s90"
- .clearfix
- %h2
- = @group.name
- - if @group.description.present?
- %p
- = escaped_autolink(@group.description)
+ .header-with-avatar.clearfix
+ = image_tag group_icon(@group), class: "avatar group-avatar s90"
+ %h3
+ = @group.name
+ .username
+ @#{@group.path}
+ - if @group.description.present?
+ .description
+ = escaped_autolink(@group.description)
%hr
+
+ = render 'shared/show_aside'
+
.row
%section.activities.col-md-8
- - if current_user
- = render "events/event_last_push", event: @last_push
- = render 'shared/event_filter'
+ .hidden-xs
+ - if current_user
+ = render "events/event_last_push", event: @last_push
+
+ - if current_user
+ %ul.nav.nav-pills.event_filter.pull-right
+ %li
+ = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do
+ %i.fa.fa-rss
+ Activity Feed
+
+ = render 'shared/event_filter'
+ %hr
+
.content_list
= spinner
%aside.side.col-md-4
= render "projects", projects: @projects
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 7b21ca30d8c..ae072bacfb1 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -187,7 +187,11 @@
%td.shortcut
.key m
%td Change milestone
- %tbody{ class: 'hidden-shortcut merge_reuests', style: 'display:none' }
+ %tr
+ %td.shortcut
+ .key r
+ %td Reply (quoting selected text)
+ %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
%tr
%th
%th Merge Requests
@@ -199,6 +203,10 @@
%td.shortcut
.key m
%td Change milestone
+ %tr
+ %td.shortcut
+ .key r
+ %td Reply (quoting selected text)
:javascript
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index eca34dbff06..8551496b98a 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,2 +1,3 @@
+- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize)
.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/help/ui.html.haml b/app/views/help/ui.html.haml
index 58de5b7c869..7c89457ace3 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -1,3 +1,4 @@
+- page_title "UI Development Kit", "Help"
- lorem = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum nisi sapien, non consequat lectus aliquam ultrices. Suspendisse sodales est euismod nunc condimentum, a consectetur diam ornare."
.gitlab-ui-dev-kit
@@ -22,6 +23,8 @@
%li
= link_to 'Forms', '#forms'
%li
+ = link_to 'Files', '#file'
+ %li
= link_to 'Markdown', '#markdown'
%h2#blocks Blocks
@@ -51,7 +54,7 @@
%code .panel .well-list
.panel.panel-default
- .panel-heading My list
+ .panel-heading Your list
%ul.well-list
%li
One item
@@ -193,6 +196,23 @@
Remember me
%button.btn.btn-default{:type => "submit"} Sign in
+ %h2#file File
+ %h3
+ %code .file-holder
+
+ - blob = Snippet.new(content: "Wow\nSuch\nFile")
+ .example
+ .file-holder
+ .file-title
+ Awesome file
+ .file-actions
+ .btn-group
+ %a.btn Edit
+ %a.btn Remove
+ .file-contenta.code
+ = render 'shared/file_highlight', blob: blob
+
+
%h2#markdown Markdown
%h3
%code .md or .wiki and others
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/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index bcbbaadf3e0..9d2858e4e72 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,3 +1,4 @@
+- page_title "Bitbucket import"
%h3.page-title
%i.fa.fa-bitbucket
Import projects from Bitbucket
@@ -23,7 +24,7 @@
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
- %span.cgreen
+ %span
%i.fa.fa-check
done
- elsif project.import_status == 'started'
@@ -42,5 +43,4 @@
= button_tag "Import", class: "btn js-add-to-import"
:coffeescript
- $ ->
- new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}")
+ new ImporterStatus("#{jobs_import_bitbucket_path}", "#{import_bitbucket_path}")
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 883090a3026..ef552498239 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -1,3 +1,4 @@
+- page_title "GitHub import"
%h3.page-title
%i.fa.fa-github
Import projects from GitHub
@@ -23,7 +24,7 @@
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
- %span.cgreen
+ %span
%i.fa.fa-check
done
- elsif project.import_status == 'started'
@@ -42,5 +43,4 @@
= button_tag "Import", class: "btn js-add-to-import"
:coffeescript
- $ ->
- new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}")
+ new ImporterStatus("#{jobs_import_github_path}", "#{import_github_path}")
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index 41ac073eae1..727f3c7e7fa 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -1,3 +1,4 @@
+- page_title "GitLab.com import"
%h3.page-title
%i.fa.fa-heart
Import projects from GitLab.com
@@ -23,7 +24,7 @@
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
- %span.cgreen
+ %span
%i.fa.fa-check
done
- elsif project.import_status == 'started'
@@ -42,5 +43,4 @@
= button_tag "Import", class: "btn js-add-to-import"
:coffeescript
- $ ->
- new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}")
+ new ImporterStatus("#{jobs_import_gitlab_path}", "#{import_gitlab_path}")
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
index ebe24747a05..bff7ee7c85d 100644
--- a/app/views/import/gitorious/status.html.haml
+++ b/app/views/import/gitorious/status.html.haml
@@ -1,3 +1,4 @@
+- page_title "Gitorious import"
%h3.page-title
%i.icon-gitorious.icon-gitorious-big
Import projects from Gitorious.org
@@ -23,7 +24,7 @@
%strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
- if project.import_status == 'finished'
- %span.cgreen
+ %span
%i.fa.fa-check
done
- elsif project.import_status == 'started'
@@ -42,5 +43,4 @@
= button_tag "Import", class: "btn js-add-to-import"
:coffeescript
- $ ->
- new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}")
+ new ImporterStatus("#{jobs_import_gitorious_path}", "#{import_gitorious_path}")
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
new file mode 100644
index 00000000000..9c64e0a009f
--- /dev/null
+++ b/app/views/import/google_code/new.html.haml
@@ -0,0 +1,61 @@
+- page_title "Google Code import"
+%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..e53ebda7dc1
--- /dev/null
+++ b/app/views/import/google_code/new_user_map.html.haml
@@ -0,0 +1,43 @@
+- page_title "User map", "Google Code import"
+%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 the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side.
+ %ul
+ %li
+ %strong Default: Directly import the Google Code email address or username
+ %p
+ <code>"johnsmith@example.com": "johnsm...@example.com"</code>
+ will add "By johnsm...@example.com" to all issues and comments originally created by johnsmith@example.com.
+ The email address or username is masked to ensure the user's privacy.
+ %li
+ %strong Map a Google Code user to a GitLab user
+ %p
+ <code>"johnsmith@example.com": "@johnsmith"</code>
+ will add "By <a href="#">@johnsmith</a>" to all issues and comments originally created by johnsmith@example.com,
+ and will set <a href="#">@johnsmith</a> as the assignee on all issues originally assigned to johnsmith@example.com.
+ %li
+ %strong Map a Google Code user to a full name
+ %p
+ <code>"johnsmith@example.com": "John Smith"</code>
+ will add "By John Smith" to all issues and comments originally created by johnsmith@example.com.
+ %li
+ %strong Map a Google Code user to a full email address
+ %p
+ <code>"johnsmith@example.com": "johnsmith@example.com"</code>
+ will add "By <a href="#">johnsmith@example.com</a>" to all issues and comments originally created by johnsmith@example.com.
+ By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address.
+
+ .form-group
+ .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..e8ec79e72f7
--- /dev/null
+++ b/app/views/import/google_code/status.html.haml
@@ -0,0 +1,70 @@
+- page_title "Google Code import"
+%h3.page-title
+ %i.fa.fa-google
+ Import projects from Google Code
+
+- if @repos.any?
+ %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
+ - if @incompatible_repos.any?
+ = button_tag 'Import all compatible projects', class: "btn btn-success js-import-all"
+ - else
+ = 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"
+ - @incompatible_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
+ %td.import-actions-job-status
+ = label_tag "Incompatible Project", nil, class: "label label-danger"
+
+- if @incompatible_repos.any?
+ %p
+ One or more of your Google Code projects cannot be imported into GitLab
+ directly because they use Subversion or Mercurial for version control,
+ rather than Git. Please convert them to Git on Google Code, and go
+ through the
+ = link_to "import flow", new_import_google_code_path
+ again.
+
+: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..2fd4859c1c6
--- /dev/null
+++ b/app/views/invites/show.html.haml
@@ -0,0 +1,30 @@
+- page_title "Invitation"
+%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/_bootlint.haml b/app/views/layouts/_bootlint.haml
new file mode 100644
index 00000000000..69280687a9d
--- /dev/null
+++ b/app/views/layouts/_bootlint.haml
@@ -0,0 +1,4 @@
+:javascript
+ jQuery(document).ready(function() {
+ javascript:(function(){var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s)})();
+ });
diff --git a/app/views/layouts/_empty_head_panel.html.haml b/app/views/layouts/_empty_head_panel.html.haml
new file mode 100644
index 00000000000..358caa3868b
--- /dev/null
+++ b/app/views/layouts/_empty_head_panel.html.haml
@@ -0,0 +1,4 @@
+%header.navbar.navbar-fixed-top.navbar-gitlab
+ .container
+ %h4.center
+ = image_tag 'logo-white.png', width: 32, height: 32
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index d12145651af..b1a57d9824e 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -1,10 +1,11 @@
+- page_title "GitLab"
%head
%meta{charset: "utf-8"}
+ %meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
%meta{content: "GitLab Community Edition", name: "description"}
- %title
- = "#{title} | " if defined?(title)
- GitLab
+ %title= page_title
+
= favicon_link_tag 'favicon.ico'
= stylesheet_link_tag "application", :media => "all"
= stylesheet_link_tag "print", :media => "print"
@@ -14,15 +15,8 @@
%meta{name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1'}
%meta{name: 'theme-color', content: '#474D57'}
+ = yield(:meta_tags)
+
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
-
- -# Atom feed
- - if current_user
- - if controller_name == 'projects' && action_name == 'index'
- = auto_discovery_link_tag :atom, projects_url(:atom, private_token: current_user.private_token), title: "Dashboard feed"
- - if @project && !@project.new_record?
- - if current_controller?(:tree, :commits)
- = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "Recent commits to #{@project.name}:#{@ref}")
- - if current_controller?(:issues)
- = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
+ = render 'layouts/bootlint' if Rails.env.development?
diff --git a/app/views/layouts/_head_panel.html.haml b/app/views/layouts/_head_panel.html.haml
index fc8a487ece7..ef685a0434e 100644
--- a/app/views/layouts/_head_panel.html.haml
+++ b/app/views/layouts/_head_panel.html.haml
@@ -1,48 +1,48 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab
- .navbar-inner
- .container
- %div.app_logo
- = link_to root_path, class: "home has_bottom_tooltip", title: "Dashboard" do
- = brand_header_logo
- %h1.title= title
+%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+ .container
+ %div.app_logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = brand_header_logo
+ %h3 GitLab
+ %h1.title
+ = title
- %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"}
- %span.sr-only Toggle navigation
- %i.fa.fa-bars
+ %button.navbar-toggle{type: 'button', data: {target: '.navbar-collapse', toggle: 'collapse'}}
+ %span.sr-only Toggle navigation
+ = icon('bars')
- .navbar-collapse.collapse
- %ul.nav.navbar-nav
- %li.hidden-sm.hidden-xs
- = render "layouts/search"
- %li.visible-sm.visible-xs
- = link_to search_path, title: "Search", class: 'has_bottom_tooltip', 'data-original-title' => 'Search area' do
- %i.fa.fa-search
+ .navbar-collapse.collapse
+ %ul.nav.navbar-nav
+ %li.hidden-sm.hidden-xs
+ = render 'layouts/search'
+ %li.visible-sm.visible-xs
+ = link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('search')
+ %li
+ = link_to help_path, title: 'Help', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('question-circle')
+ %li
+ = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('globe')
+ %li
+ = link_to user_snippets_path(current_user), title: 'Your snippets', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('clipboard')
+ - if current_user.is_admin?
%li
- = link_to help_path, title: 'Help', class: 'has_bottom_tooltip',
- 'data-original-title' => 'Help' do
- %i.fa.fa-question-circle
+ = link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('cogs')
+ - if current_user.can_create_project?
%li
- = link_to explore_root_path, title: "Explore", class: 'has_bottom_tooltip', 'data-original-title' => 'Public area' do
- %i.fa.fa-globe
- %li
- = link_to user_snippets_path(current_user), title: "My snippets", class: 'has_bottom_tooltip', 'data-original-title' => 'My snippets' do
- %i.fa.fa-clipboard
- - if current_user.is_admin?
- %li
- = link_to admin_root_path, title: "Admin area", class: 'has_bottom_tooltip', 'data-original-title' => 'Admin area' do
- %i.fa.fa-cogs
- - if current_user.can_create_project?
- %li
- = link_to new_project_path, title: "New project", class: 'has_bottom_tooltip', 'data-original-title' => 'New project' do
- %i.fa.fa-plus
- %li
- = 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
- %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
- = image_tag avatar_icon(current_user.email, 60), alt: 'User activity'
+ = link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('plus')
+ %li
+ = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('user')
+ %li
+ = link_to destroy_user_session_path, class: 'logout', method: :delete, title: 'Sign out', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = icon('sign-out')
+ %li.hidden-xs
+ = link_to current_user, class: 'profile-pic', id: 'profile-pic', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = image_tag avatar_icon(current_user.email, 60), alt: 'User activity'
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 422966cdc55..5c55bdb5465 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,23 +1,17 @@
-- if defined?(sidebar)
- .page-with-sidebar{ class: nav_sidebar_class }
- = render "layouts/broadcast"
- .sidebar-wrapper
- = render(sidebar)
- .collapse-nav
- = render partial: 'layouts/collapse_button'
- .content-wrapper
- .container-fluid
- .content
- = render "layouts/flash"
- .clearfix
- = yield
-- else
- .container.navless-container
- .content
- = yield
+.page-with-sidebar{ class: nav_sidebar_class }
+ = render "layouts/broadcast"
+ .sidebar-wrapper
+ - if defined?(sidebar) && sidebar
+ = render "layouts/nav/#{sidebar}"
+ - elsif current_user
+ = render 'layouts/nav/dashboard'
+ .collapse-nav
+ = render partial: 'layouts/collapse_button'
+ .content-wrapper
+ .container-fluid
+ .content
+ = render "layouts/flash"
+ .clearfix
+ = yield
= yield :embedded_scripts
-
-:coffeescript
- $('.page-sidebar-collapsed .nav-sidebar a').tooltip placement: "right"
-
diff --git a/app/views/layouts/_public_head_panel.html.haml b/app/views/layouts/_public_head_panel.html.haml
index 3d6d2bfc00a..8a297566d6c 100644
--- a/app/views/layouts/_public_head_panel.html.haml
+++ b/app/views/layouts/_public_head_panel.html.haml
@@ -1,22 +1,22 @@
-%header.navbar.navbar-fixed-top.navbar-gitlab
- .navbar-inner
- .container
- %div.app_logo
- = link_to explore_root_path, class: "home" do
- = brand_header_logo
- %h1.title= title
+%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
+ .container
+ %div.app_logo
+ = link_to explore_root_path, class: "home" do
+ = brand_header_logo
+ %h3 GitLab
+ %h1.title= title
- %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"}
- %span.sr-only Toggle navigation
- %i.fa.fa-bars
+ %button.navbar-toggle{"data-target" => ".navbar-collapse", "data-toggle" => "collapse", type: "button"}
+ %span.sr-only Toggle navigation
+ %i.fa.fa-bars
- - unless current_controller?('sessions')
- .pull-right.hidden-xs
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10'
+ - unless current_controller?('sessions')
+ .pull-right.hidden-xs
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-new append-right-10'
- .navbar-collapse.collapse
- %ul.nav.navbar-nav
- %li.visible-xs
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes')
+ .navbar-collapse.collapse
+ %ul.nav.navbar-nav
+ %li.visible-xs
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index ab84e87c300..1c738719bd8 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,6 +1,5 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: "Admin area"
- %body{class: "#{app_theme} admin", :'data-page' => body_data_page}
- = render "layouts/head_panel", title: link_to("Admin area", admin_root_path)
- = render 'layouts/page', sidebar: 'layouts/nav/admin'
+- page_title "Admin area"
+- header_title "Admin area", admin_root_path
+- sidebar "admin"
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 6bd8ac4adb8..a97feeb1ecd 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,6 +1,10 @@
!!! 5
%html{ lang: "en"}
- = render "layouts/head", title: "Dashboard"
- %body{class: "#{app_theme} application", :'data-page' => body_data_page }
- = render "layouts/head_panel", title: link_to("Dashboard", root_path)
- = render 'layouts/page', sidebar: 'layouts/nav/dashboard'
+ = render "layouts/head"
+ %body{class: "#{app_theme}", :'data-page' => body_data_page}
+ - if current_user
+ = render "layouts/head_panel", title: header_title
+ - else
+ = render "layouts/public_head_panel", title: header_title
+
+ = render 'layouts/page', sidebar: sidebar
diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml
new file mode 100644
index 00000000000..c72eca10bf4
--- /dev/null
+++ b/app/views/layouts/dashboard.html.haml
@@ -0,0 +1,5 @@
+- page_title "Dashboard"
+- header_title "Dashboard", root_path
+- sidebar "dashboard"
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 6f805f1c9d1..5a59c9fd59a 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -2,8 +2,8 @@
%html{ lang: "en"}
= render "layouts/head"
%body.ui_mars.login-page.application
+ = render "layouts/empty_head_panel"
= render "layouts/broadcast"
- = render "layouts/public_head_panel", title: ''
.container.navless-container
.content
= render "layouts/flash"
diff --git a/app/views/layouts/doorkeeper/admin.html.haml b/app/views/layouts/doorkeeper/admin.html.haml
deleted file mode 100644
index bd9adfab66d..00000000000
--- a/app/views/layouts/doorkeeper/admin.html.haml
+++ /dev/null
@@ -1,22 +0,0 @@
-!!!
-%html
- %head
- %meta{:charset => "utf-8"}
- %meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}
- %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
- %title Doorkeeper
- = stylesheet_link_tag "doorkeeper/admin/application"
- = csrf_meta_tags
- %body
- .navbar.navbar-inverse.navbar-fixed-top{:role => "navigation"}
- .container
- .navbar-header
- = link_to 'OAuth2 Provider', oauth_applications_path, class: 'navbar-brand'
- %ul.nav.navbar-nav
- = content_tag :li, class: "#{'active' if request.path == oauth_applications_path}" do
- = link_to 'Applications', oauth_applications_path
- .container
- - if flash[:notice].present?
- .alert.alert-info
- = flash[:notice]
- = yield \ No newline at end of file
diff --git a/app/views/layouts/doorkeeper/application.html.haml b/app/views/layouts/doorkeeper/application.html.haml
deleted file mode 100644
index e5f37fad1f4..00000000000
--- a/app/views/layouts/doorkeeper/application.html.haml
+++ /dev/null
@@ -1,15 +0,0 @@
-!!!
-%html
- %head
- %title OAuth authorize required
- %meta{:charset => "utf-8"}
- %meta{:content => "IE=edge", "http-equiv" => "X-UA-Compatible"}
- %meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}
- = stylesheet_link_tag "doorkeeper/application"
- = csrf_meta_tags
- %body
- #container
- - if flash[:notice].present?
- .alert.alert-info
- = flash[:notice]
- = yield \ No newline at end of file
diff --git a/app/views/layouts/errors.html.haml b/app/views/layouts/errors.html.haml
index e51fd4cb820..aa0f3f0a819 100644
--- a/app/views/layouts/errors.html.haml
+++ b/app/views/layouts/errors.html.haml
@@ -1,6 +1,6 @@
!!! 5
%html{ lang: "en"}
- = render "layouts/head", title: "Error"
+ = render "layouts/head"
%body{class: "#{app_theme} application"}
= render "layouts/head_panel", title: "" if current_user
.container.navless-container
diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml
index 2bd0b8d85c9..56bb92a536e 100644
--- a/app/views/layouts/explore.html.haml
+++ b/app/views/layouts/explore.html.haml
@@ -1,30 +1,5 @@
-- page_title = 'Explore'
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: page_title
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/broadcast"
- - if current_user
- = render "layouts/head_panel", title: link_to(page_title, explore_root_path)
- - else
- = render "layouts/public_head_panel", title: link_to(page_title, explore_root_path)
- .container.navless-container
- .content
- .explore-title
- %h3
- Explore GitLab
- %p.lead
- Discover projects and groups. Share your projects with others
+- page_title "Explore"
+- header_title "Explore GitLab", explore_root_path
+- sidebar "explore"
-
- %ul.nav.nav-tabs
- = nav_link(path: 'projects#trending') do
- = link_to 'Trending Projects', explore_root_path
- = nav_link(path: 'projects#starred') do
- = link_to 'Most Starred Projects', starred_explore_projects_path
- = nav_link(path: 'projects#index') do
- = link_to 'All Projects', explore_projects_path
- = nav_link(controller: :groups) do
- = link_to 'All Groups', explore_groups_path
-
- = yield
+= render template: "layouts/application"
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index f4a6bee15f6..5edc03129d2 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,6 +1,5 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: group_head_title
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/head_panel", title: link_to(@group.name, group_path(@group))
- = render 'layouts/page', sidebar: 'layouts/nav/group'
+- page_title @group.name
+- header_title @group.name, group_path(@group)
+- sidebar "group"
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/help.html.haml b/app/views/layouts/help.html.haml
new file mode 100644
index 00000000000..224b24befbe
--- /dev/null
+++ b/app/views/layouts/help.html.haml
@@ -0,0 +1,4 @@
+- page_title "Help"
+- header_title "Help", help_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 2f38d596c65..a3191593dae 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,60 +1,64 @@
%ul.nav.nav-sidebar
= nav_link(controller: :dashboard, html_options: {class: 'home'}) do
= link_to admin_root_path, title: "Stats" do
- %i.fa.fa-dashboard
+ = icon('dashboard fw')
%span
Overview
= nav_link(controller: :projects) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- %i.fa.fa-cube
+ = link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do
+ = icon('cube fw')
%span
Projects
= nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %i.fa.fa-user
+ = link_to admin_users_path, title: 'Users', data: {placement: 'right'} do
+ = icon('user fw')
%span
Users
= nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %i.fa.fa-group
+ = link_to admin_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = icon('group fw')
%span
Groups
+ = nav_link(controller: :deploy_keys) do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys', data: {placement: 'right'} do
+ = icon('key fw')
+ %span
+ Deploy Keys
= nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %i.fa.fa-file-text
+ = link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do
+ = icon('file-text fw')
%span
Logs
= nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Broadcast Messages' do
- %i.fa.fa-bullhorn
+ = link_to admin_broadcast_messages_path, title: 'Broadcast Messages', data: {placement: 'right'} do
+ = icon('bullhorn fw')
%span
Messages
= nav_link(controller: :hooks) do
- = link_to admin_hooks_path, title: 'Hooks' do
- %i.fa.fa-external-link
+ = link_to admin_hooks_path, title: 'Hooks', data: {placement: 'right'} do
+ = icon('external-link fw')
%span
Hooks
= nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %i.fa.fa-cog
+ = link_to admin_background_jobs_path, title: 'Background Jobs', data: {placement: 'right'} do
+ = icon('cog fw')
%span
Background Jobs
= nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications' do
- %i.fa.fa-cloud
+ = link_to admin_applications_path, title: 'Applications', data: {placement: 'right'} do
+ = icon('cloud fw')
%span
Applications
= nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' do
- %i.fa.fa-copy
+ = link_to admin_application_settings_services_path, title: 'Service Templates', data: {placement: 'right'} do
+ = icon('copy fw')
%span
Service Templates
= nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
- = link_to admin_application_settings_path, title: 'Settings' do
- %i.fa.fa-cogs
+ = link_to admin_application_settings_path, title: 'Settings', data: {placement: 'right'} do
+ = icon('cogs fw')
%span
Settings
-
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index b21f25e87cf..d46dba4a240 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,44 +1,38 @@
%ul.nav.nav-sidebar
= nav_link(path: 'dashboard#show', html_options: {class: 'home'}) do
- = link_to root_path, title: 'Home', class: 'shortcuts-activity' do
- %i.fa.fa-dashboard
+ = link_to root_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
+ = icon('dashboard fw')
%span
- Activity
- = nav_link(path: 'dashboard#projects') do
- = link_to projects_dashboard_path, title: 'Projects', class: 'shortcuts-projects' do
- %i.fa.fa-cube
- %span
- Projects
+ Your Projects
= nav_link(path: 'projects#starred') do
- = link_to starred_dashboard_projects_path, title: 'Starred Projects' do
- %i.fa.fa-star
+ = link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
+ = icon('star fw')
%span
Starred Projects
= nav_link(controller: :groups) do
- = link_to dashboard_groups_path, title: 'Groups' do
- %i.fa.fa-group
+ = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = icon('group fw')
%span
Groups
= nav_link(controller: :milestones) do
- = link_to dashboard_milestones_path, title: 'Milestones' do
- %i.fa.fa-clock-o
+ = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do
+ = icon('clock-o fw')
%span
Milestones
= nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues' do
- %i.fa.fa-exclamation-circle
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
+ = icon('exclamation-circle fw')
%span
Issues
%span.count= current_user.assigned_issues.opened.count
= nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- %i.fa.fa-tasks
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do
+ = icon('tasks fw')
%span
Merge Requests
%span.count= current_user.assigned_merge_requests.opened.count
= nav_link(controller: :help) do
- = link_to help_path, title: 'Help' do
- %i.fa.fa-question-circle
+ = link_to help_path, title: 'Help', data: {placement: 'right'} do
+ = icon('question-circle fw')
%span
Help
-
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
new file mode 100644
index 00000000000..66870e84ceb
--- /dev/null
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -0,0 +1,18 @@
+%ul.nav.nav-sidebar
+ = nav_link(path: 'projects#trending') do
+ = link_to explore_root_path, title: 'Trending Projects', data: {placement: 'right'} do
+ = icon('comments fw')
+ %span Trending Projects
+ = nav_link(path: 'projects#starred') do
+ = link_to starred_explore_projects_path, title: 'Most-starred Projects', data: {placement: 'right'} do
+ = icon('star fw')
+ %span Most-starred Projects
+ = nav_link(path: 'projects#index') do
+ = link_to explore_projects_path, title: 'All Projects', data: {placement: 'right'} do
+ = icon('bookmark fw')
+ %span All Projects
+ = nav_link(controller: :groups) do
+ = link_to explore_groups_path, title: 'All Groups', data: {placement: 'right'} do
+ = icon('group fw')
+ %span All Groups
+
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index ddd3df19eec..62f0579d48b 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -1,42 +1,53 @@
%ul.nav.nav-sidebar
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
- = link_to group_path(@group), title: "Home" do
- %i.fa.fa-dashboard
+ = link_to group_path(@group), title: 'Home', data: {placement: 'right'} do
+ = icon('dashboard fw')
%span
Activity
- if current_user
= nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones' do
- %i.fa.fa-clock-o
+ = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do
+ = icon('clock-o fw')
%span
Milestones
= nav_link(path: 'groups#issues') do
- = link_to issues_group_path(@group), title: 'Issues' do
- %i.fa.fa-exclamation-circle
+ = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do
+ = icon('exclamation-circle fw')
%span
Issues
- if current_user
%span.count= Issue.opened.of_group(@group).count
= nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests' do
- %i.fa.fa-tasks
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do
+ = icon('tasks fw')
%span
Merge Requests
- if current_user
%span.count= MergeRequest.opened.of_group(@group).count
- = nav_link(path: 'groups#members') do
- = link_to members_group_path(@group), title: 'Members' do
- %i.fa.fa-users
+ = nav_link(controller: [:group_members]) do
+ = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do
+ = icon('users fw')
%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
+ = link_to edit_group_path(@group), title: 'Settings', class: 'tab no-highlight', data: {placement: 'right'} do
+ = icon ('cogs fw')
%span
Settings
- %i.fa.fa-angle-down
+ = icon ('angle-down fw')
- if group_settings_page?
- = render 'groups/settings_nav'
+ %ul.sidebar-subnav
+ = nav_link(path: 'groups#edit') do
+ = link_to edit_group_path(@group), title: 'Group', data: {placement: 'right'} do
+ = icon('pencil-square-o')
+ %span
+ Group
+ = nav_link(path: 'groups#projects') do
+ = link_to projects_group_path(@group), title: 'Projects', data: {placement: 'right'} do
+ = icon('folder')
+ %span
+ Projects
+
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index d88e862829d..31d8ed3ed86 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,50 +1,50 @@
%ul.nav.nav-sidebar
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
- = link_to profile_path, title: "Profile" do
- %i.fa.fa-user
+ = link_to profile_path, title: 'Profile', data: {placement: 'right'} do
+ = icon('user fw')
%span
Profile
= nav_link(controller: :accounts) do
- = link_to profile_account_path, title: 'Account' do
- %i.fa.fa-gear
+ = link_to profile_account_path, title: 'Account', data: {placement: 'right'} do
+ = icon('gear fw')
%span
Account
= nav_link(path: ['profiles#applications', 'applications#edit', 'applications#show', 'applications#new']) do
- = link_to applications_profile_path, title: 'Applications' do
- %i.fa.fa-cloud
+ = link_to applications_profile_path, title: 'Applications', data: {placement: 'right'} do
+ = icon('cloud fw')
%span
Applications
= nav_link(controller: :emails) do
- = link_to profile_emails_path, title: 'Emails' do
- %i.fa.fa-envelope-o
+ = link_to profile_emails_path, title: 'Emails', data: {placement: 'right'} do
+ = icon('envelope-o fw')
%span
Emails
%span.count= current_user.emails.count + 1
- unless current_user.ldap_user?
= nav_link(controller: :passwords) do
- = link_to edit_profile_password_path, title: 'Password' do
- %i.fa.fa-lock
+ = link_to edit_profile_password_path, title: 'Password', data: {placement: 'right'} do
+ = icon('lock fw')
%span
Password
= nav_link(controller: :notifications) do
- = link_to profile_notifications_path, title: 'Notifications' do
- %i.fa.fa-inbox
+ = link_to profile_notifications_path, title: 'Notifications', data: {placement: 'right'} do
+ = icon('inbox fw')
%span
Notifications
= nav_link(controller: :keys) do
- = link_to profile_keys_path, title: 'SSH Keys' do
- %i.fa.fa-key
+ = link_to profile_keys_path, title: 'SSH Keys', data: {placement: 'right'} do
+ = icon('key fw')
%span
SSH Keys
%span.count= current_user.keys.count
= nav_link(path: 'profiles#design') do
- = link_to design_profile_path, title: 'Design' do
- %i.fa.fa-image
+ = link_to design_profile_path, title: 'Design', data: {placement: 'right'} do
+ = icon('image fw')
%span
Design
= nav_link(path: 'profiles#history') do
- = link_to history_profile_path, title: 'History' do
- %i.fa.fa-history
+ = link_to history_profile_path, title: 'History', data: {placement: 'right'} do
+ = icon('history fw')
%span
History
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index d340ab1796a..172f5197b24 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -1,95 +1,85 @@
%ul.project-navigation.nav.nav-sidebar
- - if @project_settings_nav
- = nav_link do
- = link_to project_path(@project), title: 'Back to project', class: "" do
- %i.fa.fa-caret-square-o-left
+ = nav_link(path: 'projects#show', html_options: {class: 'home'}) do
+ = link_to project_path(@project), title: 'Project', class: 'shortcuts-project', data: {placement: 'right'} do
+ = icon('dashboard fw')
+ %span
+ Project
+ - if project_nav_tab? :files
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
+ = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do
+ = icon('files-o fw')
%span
- Back to project
+ Files
- %li.separate-item
-
- = render 'projects/settings_nav'
-
- - else
- = nav_link(path: 'projects#show', html_options: {class: "home"}) do
- = link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
- %i.fa.fa-dashboard
+ - if project_nav_tab? :commits
+ = nav_link(controller: %w(commit commits compare repositories tags branches)) do
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do
+ = icon('history fw')
%span
- Project
- - if project_nav_tab? :files
- = nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
- = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree' do
- %i.fa.fa-files-o
- %span
- Files
-
- - if project_nav_tab? :commits
- = nav_link(controller: %w(commit commits compare repositories tags branches)) do
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits' do
- %i.fa.fa-history
- %span
- Commits
+ Commits
- - if project_nav_tab? :network
- = nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network' do
- %i.fa.fa-code-fork
- %span
- Network
+ - if project_nav_tab? :network
+ = nav_link(controller: %w(network)) do
+ = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do
+ = icon('code-fork fw')
+ %span
+ Network
- - if project_nav_tab? :graphs
- = nav_link(controller: %w(graphs)) do
- = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs' do
- %i.fa.fa-area-chart
- %span
- Graphs
+ - if project_nav_tab? :graphs
+ = nav_link(controller: %w(graphs)) do
+ = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do
+ = icon('area-chart fw')
+ %span
+ Graphs
+ - if project_nav_tab? :milestones
= nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- %i.fa.fa-clock-o
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones', data: {placement: 'right'} do
+ = icon('clock-o fw')
%span
Milestones
- - if project_nav_tab? :issues
- = nav_link(controller: :issues) do
- = link_to url_for_project_issues, title: 'Issues', class: 'shortcuts-issues' do
- %i.fa.fa-exclamation-circle
- %span
- Issues
- - if @project.default_issues_tracker?
- %span.count.issue_counter= @project.issues.opened.count
+ - if project_nav_tab? :issues
+ = nav_link(controller: :issues) do
+ = link_to url_for_project_issues(@project, only_path: true), title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
+ = icon('exclamation-circle fw')
+ %span
+ Issues
+ - if @project.default_issues_tracker?
+ %span.count.issue_counter= @project.issues.opened.count
- - if project_nav_tab? :merge_requests
- = nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
- %i.fa.fa-tasks
- %span
- Merge Requests
- %span.count.merge_counter= @project.merge_requests.opened.count
+ - if project_nav_tab? :merge_requests
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ %span.count.merge_counter= @project.merge_requests.opened.count
+ - if project_nav_tab? :labels
= nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- %i.fa.fa-tags
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels', data: {placement: 'right'} do
+ = icon('tags fw')
%span
Labels
- - if project_nav_tab? :wiki
- = nav_link(controller: :wikis) do
- = link_to namespace_project_wiki_path(@project.namespace, @project, :home), title: 'Wiki', class: 'shortcuts-wiki' do
- %i.fa.fa-book
- %span
- Wiki
+ - if project_nav_tab? :wiki
+ = nav_link(controller: :wikis) do
+ = link_to get_project_wiki_path(@project), title: 'Wiki', class: 'shortcuts-wiki', data: {placement: 'right'} do
+ = icon('book fw')
+ %span
+ Wiki
- - if project_nav_tab? :snippets
- = nav_link(controller: :snippets) do
- = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets' do
- %i.fa.fa-file-text-o
- %span
- Snippets
+ - if project_nav_tab? :snippets
+ = nav_link(controller: :snippets) do
+ = link_to namespace_project_snippets_path(@project.namespace, @project), title: 'Snippets', class: 'shortcuts-snippets', data: {placement: 'right'} do
+ = icon('file-text-o fw')
+ %span
+ Snippets
- - if project_nav_tab? :settings
- = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
- = link_to edit_project_path(@project), title: 'Settings', class: "stat-tab tab no-highlight" do
- %i.fa.fa-cogs
- %span
- Settings
+ - if project_nav_tab? :settings
+ = nav_link(html_options: {class: "#{project_tab_class} separate-item"}) do
+ = link_to edit_project_path(@project), title: 'Settings', class: 'stat-tab tab no-highlight', data: {placement: 'right'} do
+ = icon('cogs fw')
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
new file mode 100644
index 00000000000..21260302a09
--- /dev/null
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -0,0 +1,41 @@
+%ul.project-navigation.nav.nav-sidebar
+ = nav_link do
+ = link_to project_path(@project), title: 'Back to project', data: {placement: 'right'} do
+ = icon('caret-square-o-left fw')
+ %span
+ Back to project
+
+ %li.separate-item
+
+ %ul.project-settings-nav.sidebar-subnav
+ = nav_link(path: 'projects#edit') do
+ = link_to edit_project_path(@project), title: 'Project', class: 'stat-tab tab', data: {placement: 'right'} do
+ = icon('pencil-square-o')
+ %span
+ Project
+ = nav_link(controller: [:project_members, :teams]) do
+ = link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do
+ = icon('users')
+ %span
+ Members
+ = nav_link(controller: :deploy_keys) do
+ = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys', data: {placement: 'right'} do
+ = icon('key')
+ %span
+ Deploy Keys
+ = nav_link(controller: :hooks) do
+ = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks', data: {placement: 'right'} do
+ = icon('link')
+ %span
+ Web Hooks
+ = nav_link(controller: :services) do
+ = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services', data: {placement: 'right'} do
+ = icon('cogs')
+ %span
+ Services
+ = nav_link(controller: :protected_branches) do
+ = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches', data: {placement: 'right'} do
+ = icon('lock')
+ %span
+ Protected branches
+
diff --git a/app/views/layouts/nav/_snippets.html.haml b/app/views/layouts/nav/_snippets.html.haml
new file mode 100644
index 00000000000..458b76a2c99
--- /dev/null
+++ b/app/views/layouts/nav/_snippets.html.haml
@@ -0,0 +1,12 @@
+%ul.nav.nav-sidebar
+ - if current_user
+ = nav_link(path: user_snippets_path(current_user), html_options: {class: 'home'}) do
+ = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do
+ = icon('dashboard fw')
+ %span
+ Your Snippets
+ = nav_link(path: snippets_path) do
+ = link_to snippets_path, title: 'Discover snippets', data: {placement: 'right'} do
+ = icon('globe fw')
+ %span
+ Discover Snippets
diff --git a/app/views/layouts/navless.html.haml b/app/views/layouts/navless.html.haml
deleted file mode 100644
index 4d0278251a6..00000000000
--- a/app/views/layouts/navless.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: @title
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/broadcast"
- = render "layouts/head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title
- .container.navless-container
- .content
- = render "layouts/flash"
- = yield
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/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index 2b5be7fc372..9799b4cc4d7 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,6 +1,5 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: "Profile"
- %body{class: "#{app_theme} profile", :'data-page' => body_data_page}
- = render "layouts/head_panel", title: link_to("Profile", profile_path)
- = render 'layouts/page', sidebar: 'layouts/nav/profile'
+- page_title "Profile"
+- header_title "Profile", profile_path
+- sidebar "profile"
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
new file mode 100644
index 00000000000..4aeb9d397d2
--- /dev/null
+++ b/app/views/layouts/project.html.haml
@@ -0,0 +1,8 @@
+- page_title @project.name_with_namespace
+- header_title project_title(@project)
+- sidebar "project" unless sidebar
+
+- content_for :embedded_scripts do
+ = render "layouts/init_auto_complete" if current_user
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml
index 0a0039dec16..43401668334 100644
--- a/app/views/layouts/project_settings.html.haml
+++ b/app/views/layouts/project_settings.html.haml
@@ -1,8 +1,4 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: @project.name_with_namespace
- %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id }
- = render "layouts/head_panel", title: project_title(@project)
- = render "layouts/init_auto_complete"
- - @project_settings_nav = true
- = render 'layouts/page', sidebar: 'layouts/nav/project'
+- page_title "Settings"
+- sidebar "project_settings"
+
+= render template: "layouts/project"
diff --git a/app/views/layouts/projects.html.haml b/app/views/layouts/projects.html.haml
deleted file mode 100644
index dde0964f47f..00000000000
--- a/app/views/layouts/projects.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: project_head_title
- %body{class: "#{app_theme} project", :'data-page' => body_data_page, :'data-project-id' => @project.id }
- = render "layouts/head_panel", title: project_title(@project)
- = render "layouts/init_auto_complete"
- = render 'layouts/page', sidebar: 'layouts/nav/project'
diff --git a/app/views/layouts/public_group.html.haml b/app/views/layouts/public_group.html.haml
deleted file mode 100644
index b9b1d03e08e..00000000000
--- a/app/views/layouts/public_group.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: group_head_title
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/public_head_panel", title: link_to(@group.name, group_path(@group))
- = render 'layouts/page', sidebar: 'layouts/nav/group'
diff --git a/app/views/layouts/public_projects.html.haml b/app/views/layouts/public_projects.html.haml
deleted file mode 100644
index 04fa7c84e73..00000000000
--- a/app/views/layouts/public_projects.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: @project.name_with_namespace
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/public_head_panel", title: project_title(@project)
- = render 'layouts/page', sidebar: 'layouts/nav/project'
diff --git a/app/views/layouts/public_users.html.haml b/app/views/layouts/public_users.html.haml
deleted file mode 100644
index 71c16bd1684..00000000000
--- a/app/views/layouts/public_users.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: @title
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/public_head_panel", title: defined?(@title_url) ? link_to(@title, @title_url) : @title
- = render 'layouts/page'
diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml
index f9d8db06e10..fd4c7ad21a7 100644
--- a/app/views/layouts/search.html.haml
+++ b/app/views/layouts/search.html.haml
@@ -1,10 +1,4 @@
-!!! 5
-%html{ lang: "en"}
- = render "layouts/head", title: "Search"
- %body{class: "#{app_theme} application", :'data-page' => body_data_page}
- = render "layouts/broadcast"
- = render "layouts/head_panel", title: link_to("Search", search_path)
- .container.navless-container
- .content
- = render "layouts/flash"
- = yield
+- page_title "Search"
+- header_title "Search", search_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml
new file mode 100644
index 00000000000..9b0f40073ab
--- /dev/null
+++ b/app/views/layouts/snippets.html.haml
@@ -0,0 +1,5 @@
+- page_title 'Snippets'
+- header_title 'Snippets', snippets_path
+- sidebar "snippets"
+
+= render template: "layouts/application"
diff --git a/app/views/notify/_note_message.html.haml b/app/views/notify/_note_message.html.haml
index 5272dfa0ede..3fd4b04ac84 100644
--- a/app/views/notify/_note_message.html.haml
+++ b/app/views/notify/_note_message.html.haml
@@ -1,2 +1,2 @@
%div
- = markdown(@note.note)
+ = markdown(@note.note, reference_only_path: false)
diff --git a/app/views/notify/group_access_granted_email.html.haml b/app/views/notify/group_access_granted_email.html.haml
index 823ebf77347..f1916d624b6 100644
--- a/app/views/notify/group_access_granted_email.html.haml
+++ b/app/views/notify/group_access_granted_email.html.haml
@@ -1,4 +1,4 @@
%p
- = "You have been granted #{@membership.human_access} access to group"
+ = "You have been granted #{@group_member.human_access} access to group"
= link_to group_url(@group) do
= @group.name
diff --git a/app/views/notify/group_access_granted_email.text.erb b/app/views/notify/group_access_granted_email.text.erb
index 331bb98d5c9..ef9617bfc16 100644
--- a/app/views/notify/group_access_granted_email.text.erb
+++ b/app/views/notify/group_access_granted_email.text.erb
@@ -1,4 +1,4 @@
-You have been granted <%= @membership.human_access %> access to group <%= @group.name %>
+You have been granted <%= @group_member.human_access %> access to group <%= @group.name %>
<%= url_for(group_url(@group)) %>
diff --git a/app/views/notify/group_invite_accepted_email.html.haml b/app/views/notify/group_invite_accepted_email.html.haml
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 f2f8eee18c4..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
- = markdown(@issue.description)
+ = 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 f02d5111b22..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
- = markdown(@merge_request.description)
+ = 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/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index f53de2de287..3cd759f1f57 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -6,10 +6,10 @@
= @project.name_with_namespace
%p
To update the remote url in your local repository run (for ssh):
-%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" }
+%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
git remote set-url origin #{@project.ssh_url_to_repo}
%p
or for http(s):
-%p{ style: "background:#f5f5f5; padding:10px; border:1px solid #ddd" }
+%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
git remote set-url origin #{@project.http_url_to_repo}
%br
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index 039b92df2be..a374a662333 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -1,66 +1,66 @@
-%h3 #{@author.name} pushed to #{@branch} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
+%h3 #{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{link_to @project.name_with_namespace, namespace_project_url(@project.namespace, @project)}
-- if @reverse_compare
- %p
- %strong WARNING:
- The push did not contain any new commits, but force pushed to delete the commits and changes below.
+- if @compare
+ - if @reverse_compare
+ %p
+ %strong WARNING:
+ The push did not contain any new commits, but force pushed to delete the commits and changes below.
-%h4
- = @reverse_compare ? "Deleted commits:" : "Commits:"
+ %h4
+ = @reverse_compare ? "Deleted commits:" : "Commits:"
-%ul
- - @commits.each do |commit|
- %li
- %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)}
- %div
- %span by #{commit.author_name}
- %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
- %pre.commit-message
- = commit.safe_message
+ %ul
+ - @commits.each do |commit|
+ %li
+ %strong #{link_to commit.short_id, namespace_project_commit_url(@project.namespace, @project, commit)}
+ %div
+ %span by #{commit.author_name}
+ %i at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
+ %pre.commit-message
+ = commit.safe_message
-%h4 #{pluralize @diffs.count, "changed file"}:
+ %h4 #{pluralize @diffs.count, "changed file"}:
-%ul
- - @diffs.each_with_index do |diff, i|
- %li.file-stats
- %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" }
- - if diff.deleted_file
- %span.deleted-file
- &minus;
+ %ul
+ - @diffs.each_with_index do |diff, i|
+ %li.file-stats
+ %a{href: "#{@target_url if @disable_diffs}#diff-#{i}" }
+ - if diff.deleted_file
+ %span.deleted-file
+ &minus;
+ = diff.old_path
+ - elsif diff.renamed_file
= diff.old_path
- - elsif diff.renamed_file
- = diff.old_path
- &rarr;
- = diff.new_path
- - elsif diff.new_file
- %span.new-file
- &plus;
+ &rarr;
= diff.new_path
- - else
- = diff.new_path
-
-- unless @disable_diffs
- %h4 Changes:
- - @diffs.each_with_index do |diff, i|
- %li{id: "diff-#{i}"}
- %a{href: @target_url + "#diff-#{i}"}
- - if diff.deleted_file
- %strong
- = diff.old_path
- deleted
- - elsif diff.renamed_file
- %strong
- = diff.old_path
- &rarr;
- %strong
+ - elsif diff.new_file
+ %span.new-file
+ &plus;
+ = diff.new_path
+ - else
= diff.new_path
- - else
- %strong
- = diff.new_path
- %hr
- %pre
+
+ - unless @disable_diffs
+ %h4 Changes:
+ - @diffs.each_with_index do |diff, i|
+ %li{id: "diff-#{i}"}
+ %a{href: @target_url + "#diff-#{i}"}
+ - if diff.deleted_file
+ %strong
+ = diff.old_path
+ deleted
+ - elsif diff.renamed_file
+ %strong
+ = diff.old_path
+ &rarr;
+ %strong
+ = diff.new_path
+ - else
+ %strong
+ = diff.new_path
+ %hr
= color_email_diff(diff.diff)
- %br
+ %br
-- if @compare.timeout
- %h5 Huge diff. To prevent performance issues changes are hidden
+ - if @compare.timeout
+ %h5 Huge diff. To prevent performance issues changes are hidden
diff --git a/app/views/notify/repository_push_email.text.haml b/app/views/notify/repository_push_email.text.haml
index 8d67a42234e..97a176ed2a3 100644
--- a/app/views/notify/repository_push_email.text.haml
+++ b/app/views/notify/repository_push_email.text.haml
@@ -1,47 +1,49 @@
-#{@author.name} pushed to #{@branch} at #{@project.name_with_namespace}
-\
-\
-- if @reverse_compare
- WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below.
+#{@author.name} #{@action_name} #{@ref_type} #{@ref_name} at #{@project.name_with_namespace}
+- if @compare
\
\
-= @reverse_compare ? "Deleted commits:" : "Commits:"
-- @commits.each do |commit|
- #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
- #{commit.safe_message}
- \- - - - -
-\
-\
-#{pluralize @diffs.count, "changed file"}:
-\
-- @diffs.each do |diff|
- - if diff.deleted_file
- \- − #{diff.old_path}
- - elsif diff.renamed_file
- \- #{diff.old_path} → #{diff.new_path}
- - elsif diff.new_file
- \- + #{diff.new_path}
- - else
- \- #{diff.new_path}
-- unless @disable_diffs
+ - if @reverse_compare
+ WARNING: The push did not contain any new commits, but force pushed to delete the commits and changes below.
+ \
+ \
+ = @reverse_compare ? "Deleted commits:" : "Commits:"
+ - @commits.each do |commit|
+ #{commit.short_id} by #{commit.author_name} at #{commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")}
+ #{commit.safe_message}
+ \- - - - -
+ \
\
+ #{pluralize @diffs.count, "changed file"}:
\
- Changes:
- @diffs.each do |diff|
- \
- \=====================================
- if diff.deleted_file
- #{diff.old_path} deleted
+ \- − #{diff.old_path}
- elsif diff.renamed_file
- #{diff.old_path} → #{diff.new_path}
+ \- #{diff.old_path} → #{diff.new_path}
+ - elsif diff.new_file
+ \- + #{diff.new_path}
- else
- = diff.new_path
- \=====================================
- != diff.diff
-- if @compare.timeout
- \
- \
- Huge diff. To prevent performance issues it was hidden
-\
-\
-View it on GitLab: #{@target_url}
+ \- #{diff.new_path}
+ - unless @disable_diffs
+ \
+ \
+ Changes:
+ - @diffs.each do |diff|
+ \
+ \=====================================
+ - if diff.deleted_file
+ #{diff.old_path} deleted
+ - elsif diff.renamed_file
+ #{diff.old_path} → #{diff.new_path}
+ - else
+ = diff.new_path
+ \=====================================
+ != diff.diff
+ - if @compare.timeout
+ \
+ \
+ Huge diff. To prevent performance issues it was hidden
+ - if @target_url
+ \
+ \
+ View it on GitLab: #{@target_url}
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 6bafcb56551..1c3a3d68aca 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,11 +1,7 @@
-%h3.page-title
- Account Settings
-%p.light
- You can change your username and private token here.
- - if current_user.ldap_user?
+- page_title "Account"
+- if current_user.ldap_user?
+ .alert.alert-info
Some options are unavailable for LDAP accounts
-%hr
-
.account-page
%fieldset.update-token
@@ -33,12 +29,16 @@
- if show_profile_social_tab?
%fieldset
- %legend Social Accounts
- .oauth_select_holder.append-bottom-10
+ %legend Connected Accounts
+ .oauth-buttons.append-bottom-10
%p Click on icon to activate signin with one of the following services
- enabled_social_providers.each do |provider|
- %span{class: oauth_active_class(provider) }
- = link_to authbutton(provider, 32), omniauth_authorize_path(User, provider)
+ .btn-group
+ = link_to oauth_image_tag(provider), omniauth_authorize_path(User, provider),
+ class: "btn btn-lg #{'active' if oauth_active?(provider)}"
+ - if oauth_active?(provider)
+ = link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
+ %i.fa.fa-close
- if show_profile_username_tab?
%fieldset.update-username
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
index c8c522e9812..c4f6f59624b 100644
--- a/app/views/profiles/applications.html.haml
+++ b/app/views/profiles/applications.html.haml
@@ -1,3 +1,4 @@
+- page_title "Applications"
%h3.page-title
Application Settings
%p.light
@@ -23,7 +24,7 @@
- application.redirect_uri.split.each do |uri|
%div= uri
%td= application.access_tokens.count
- %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-small'
+ %td= link_to 'Edit', edit_oauth_application_path(application), class: 'btn btn-link btn-sm'
%td= render 'doorkeeper/applications/delete_form', application: application
%fieldset.oauth-authorized-applications.prepend-top-20
diff --git a/app/views/profiles/design.html.haml b/app/views/profiles/design.html.haml
index 8d09595fd4f..af284f60409 100644
--- a/app/views/profiles/design.html.haml
+++ b/app/views/profiles/design.html.haml
@@ -1,3 +1,4 @@
+- page_title "Design"
%h3.page-title
Design Settings
%p.light
@@ -12,17 +13,17 @@
= label_tag do
.prev.default
= f.radio_button :theme_id, 1
- Default
+ Graphite
= label_tag do
.prev.classic
= f.radio_button :theme_id, 2
- Classic
+ Charcoal
= label_tag do
.prev.modern
= f.radio_button :theme_id, 3
- Modern
+ Green
= label_tag do
.prev.gray
@@ -33,6 +34,11 @@
.prev.violet
= f.radio_button :theme_id, 5
Violet
+
+ = label_tag do
+ .prev.blue
+ = f.radio_button :theme_id, 6
+ Blue
%br
.clearfix
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 3bbad6fdf7b..2c0d0e10a4c 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,14 +1,19 @@
+- page_title "Emails"
%h3.page-title
Email Settings
%p.light
Your
%b Primary Email
will be used for avatar detection and web based operations, such as edits and merges.
- %br
+%p.light
Your
%b Notification Email
will be used for account notifications.
- %br
+%p.light
+ Your
+ %b Public Email
+ will be displayed on your public profile.
+%p.light
All email addresses will be used to identify your commits.
%hr
@@ -20,12 +25,20 @@
%li
%strong= @primary
%span.label.label-success Primary Email
+ - if @primary === current_user.public_email
+ %span.label.label-info Public Email
+ - if @primary === current_user.notification_email
+ %span.label.label-info Notification Email
- @emails.each do |email|
%li
%strong= email.email
+ - if email.email === current_user.public_email
+ %span.label.label-info Public Email
+ - if email.email === current_user.notification_email
+ %span.label.label-info Notification Email
%span.cgray
added #{time_ago_with_tooltip(email.created_at)}
- = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-small btn-remove pull-right'
+ = link_to 'Remove', profile_email_path(email), data: { confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-sm btn-remove pull-right'
%h4 Add email address
= form_for 'email', url: profile_emails_path, html: { class: 'form-horizontal' } do |f|
diff --git a/app/views/profiles/history.html.haml b/app/views/profiles/history.html.haml
index 9cafe03b8b3..b414fb69f4e 100644
--- a/app/views/profiles/history.html.haml
+++ b/app/views/profiles/history.html.haml
@@ -1,5 +1,6 @@
+- page_title "History"
%h3.page-title
- My Account History
+ Your Account History
%p.light
All events created by your account are listed below.
%hr
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index 8892302e25d..fe5770f45c3 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -9,4 +9,4 @@
%span.cgray
added #{time_ago_with_tooltip(key.created_at)}
%td
- = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-small btn-remove delete-key pull-right"
+ = link_to 'Remove', path_to_key(key, is_admin), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-sm btn-remove delete-key pull-right"
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 965d5e032f9..e3af0d4e189 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,10 +1,9 @@
+- page_title "SSH Keys"
%h3.page-title
SSH Keys Settings
.pull-right
= link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new"
%p.light
- My SSH keys: #{@keys.count}
- %br
Before you can add an SSH key you need to
= link_to "generate it.", help_page_path("ssh", "README")
%hr
diff --git a/app/views/profiles/keys/new.html.haml b/app/views/profiles/keys/new.html.haml
index ccec716d0c6..2bf207a3221 100644
--- a/app/views/profiles/keys/new.html.haml
+++ b/app/views/profiles/keys/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "Add SSH Keys"
%h3.page-title Add an SSH Key
%p.light
Paste your public key here. Read more about how to generate a key on #{link_to "the SSH help page", help_page_path("ssh", "README")}.
diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml
index cfd53298962..89f6f01581a 100644
--- a/app/views/profiles/keys/show.html.haml
+++ b/app/views/profiles/keys/show.html.haml
@@ -1 +1,2 @@
+- page_title @key.title, "SSH Keys"
= render "key_details"
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 6cf5c81c19e..a74d97dac3b 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Notifications"
%h3.page-title
Notifications Settings
%p.light
@@ -62,9 +63,9 @@
By default, all projects and groups will use the notification level set above.
%h4 Groups:
%ul.bordered-list
- - @group_members.each do |users_group|
- - notification = Notification.new(users_group)
- = render 'settings', type: 'group', membership: users_group, notification: notification
+ - @group_members.each do |group_member|
+ - notification = Notification.new(group_member)
+ = render 'settings', type: 'group', membership: group_member, notification: notification
.col-md-6
%p
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 4b04b113e89..21dabbdfe2c 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Password"
%h3.page-title Password Settings
%p.light
- if @user.password_automatically_set?
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 8bed6e0dbee..9c6204963e0 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -1,3 +1,5 @@
+- page_title "New Password"
+- header_title "New Password"
%h3.page-title Setup new password
%hr
= form_for @user, url: profile_password_path, method: :post, html: { class: 'form-horizontal '} do |f|
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 1a7bc353bf3..29c30905117 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Settings"
%h3.page-title
Profile Settings
%p.light
@@ -42,6 +43,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
@@ -54,6 +60,9 @@
= f.label :website_url, 'Website', class: "control-label"
.col-sm-10= f.text_field :website_url, class: "form-control"
.form-group
+ = f.label :location, 'Location', class: "control-label"
+ .col-sm-10= f.text_field :location, class: "form-control"
+ .form-group
= f.label :bio, class: "control-label"
.col-sm-10
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
@@ -77,7 +86,7 @@
%br
or change it at #{link_to "gravatar.com", "http://gravatar.com"}
%hr
- %a.choose-btn.btn.btn-small.js-choose-user-avatar-button
+ %a.choose-btn.btn.btn-sm.js-choose-user-avatar-button
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
@@ -86,7 +95,7 @@
.light The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
- = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar"
+ = link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
- if @user.public_profile?
.alert.alert-info
@@ -96,5 +105,7 @@
.row
.col-md-7
- .col-sm-2
- = f.submit 'Save changes', class: "btn btn-success"
+ .form-group
+ .col-sm-2 &nbsp;
+ .col-sm-10
+ = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/profiles/update.js.erb b/app/views/profiles/update.js.erb
index e664ac2a52a..db37619136d 100644
--- a/app/views/profiles/update.js.erb
+++ b/app/views/profiles/update.js.erb
@@ -1,9 +1,3 @@
// Remove body class for any previous theme, re-add current one
-$('body').removeClass('ui_basic ui_mars ui_modern ui_gray ui_color light_theme dark_theme')
+$('body').removeClass('<%= Gitlab::Theme.body_classes %>')
$('body').addClass('<%= app_theme %> <%= theme_type %>')
-
-// Re-render the header to reflect the new theme
-$('header').html('<%= escape_javascript(render("layouts/head_panel", title: "Profile")) %>')
-
-// Re-initialize header tooltips
-$('.has_bottom_tooltip').tooltip({placement: 'bottom'})
diff --git a/app/views/projects/_aside.html.haml b/app/views/projects/_aside.html.haml
new file mode 100644
index 00000000000..1865b5be8c6
--- /dev/null
+++ b/app/views/projects/_aside.html.haml
@@ -0,0 +1,83 @@
+.clearfix
+ .append-bottom-20
+ = render "shared/clone_panel"
+
+ - unless @project.empty_repo?
+ .well
+ %h4 Repository
+ %ul.nav.nav-pills
+ %li= link_to pluralize(number_with_delimiter(@repository.commit_count), 'commit'), namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref)
+ %li= link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), namespace_project_branches_path(@project.namespace, @project)
+ %li= link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), namespace_project_tags_path(@project.namespace, @project)
+
+ .actions
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref), class: 'btn btn-sm' do
+ %i.fa.fa-exchange
+ Compare code
+
+ - if can?(current_user, :download_code, @project)
+ &nbsp;
+ = render 'projects/repositories/download_archive', split_button: true, btn_class: 'btn-group-sm'
+
+ - unless @project.empty_repo?
+ .well
+ %h4 Contribute
+ %ul.nav.nav-pills
+ - if @repository.changelog
+ %li.hidden-xs
+ = link_to changelog_url(@project) do
+ Changelog
+ - if @repository.contribution_guide
+ %li.hidden-xs
+ = link_to contribution_guide_url(@project) do
+ Contribution guide
+ - if @repository.license
+ %li
+ = link_to license_url(@project) do
+ License
+ .actions
+ = link_to url_for_new_issue(@project, only_path: true), title: "New Issue", class: 'btn btn-sm' do
+ %i.fa.fa-fw.fa-exclamation-circle
+ New issue
+ - if can? current_user, :write_merge_request, @project
+ &nbsp;
+ = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-sm", title: "New Merge Request" do
+ %i.fa.fa-plus
+ New Merge Request
+
+
+
+ - if @project.archived?
+ .alert.alert-warning
+ %h4
+ %i.fa.fa-exclamation-triangle
+ Archived project!
+ %p Repository is read-only
+
+ - if @project.forked_from_project
+ .well
+ %h4
+ Forked from
+ .pull-right
+ = link_to @project.forked_from_project.namespace.try(:name), project_path(@project.forked_from_project)
+
+
+- if version = @repository.version
+ .well
+ %h4
+ Version
+ .pull-right
+ = link_to version_url(@project) do
+ = @repository.blob_by_oid(version.id).data
+
+- @project.ci_services.each do |ci_service|
+ - if ci_service.active? && ci_service.respond_to?(:builds_path)
+ .well
+ %h4
+ = ci_service.title
+ .pull-right
+ - 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"
+ - else
+ = link_to 'view builds', ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink'
diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml
index fd8320adb8d..35f7e7bb34b 100644
--- a/app/views/projects/_commit_button.html.haml
+++ b/app/views/projects/_commit_button.html.haml
@@ -2,8 +2,5 @@
.commit-button-annotation
= button_tag 'Commit Changes',
class: 'btn commit-btn js-commit-button btn-create'
- .message
- to branch
- %strong= ref
= link_to 'Cancel', cancel_path,
class: 'btn btn-cancel', data: {confirm: leave_edit_message}
diff --git a/app/views/projects/_dropdown.html.haml b/app/views/projects/_dropdown.html.haml
index 2d5120f283b..d623a3716ed 100644
--- a/app/views/projects/_dropdown.html.haml
+++ b/app/views/projects/_dropdown.html.haml
@@ -1,33 +1,37 @@
- if current_user
.dropdown.pull-right
- %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
%i.fa.fa-bars
%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_team_member, @project)
+ - if can?(current_user, :admin_project_member, @project)
%li
- = link_to new_namespace_project_team_member_path(@project.namespace, @project), title: "New project member" do
+ = 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 c0e13e67be3..f9cdda4a3ba 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,44 +1,37 @@
- empty_repo = @project.empty_repo?
-.project-home-panel{:class => ("empty-project" if empty_repo)}
+.project-home-panel.clearfix{: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)
- if can?(current_user, :admin_project, @project)
&ndash;
= link_to 'Edit', edit_namespace_project_path
- - elsif !@project.empty_repo? && @repository.readme
+ - elsif !empty_repo && @repository.readme
- readme = @repository.readme
&ndash;
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)) do
= readme.name
- .star-fork-buttons
- - unless @project.empty_repo?
- .fork-buttons
- - if current_user && can?(current_user, :fork_project, @project) && @project.namespace != current_user.namespace
+ .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 empty_repo
+ - 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 my 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 a7cd129b631..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'
@@ -71,10 +71,10 @@
= link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank
.form-actions
- - if !issuable.project.empty_repo? && contribution_guide_url(issuable.project) && !issuable.persisted?
+ - if !issuable.project.empty_repo? && (guide_url = contribution_guide_url(issuable.project)) && !issuable.persisted?
%p
Please review the
- %strong #{link_to 'guidelines for contribution', contribution_guide_url(issuable.project)}
+ %strong #{link_to 'guidelines for contribution', guide_url}
to this repository.
- if issuable.new_record?
= f.submit "Submit new #{issuable.class.model_name.human.downcase}", class: 'btn btn-create'
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index f356a25dbfa..b869fd6e12a 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -9,5 +9,5 @@
%div
.md-write-holder
= yield
- .md-preview-holder.hide
+ .md.md-preview-holder.hide
.js-md-preview{class: (preview_class if defined?(preview_class))}
diff --git a/app/views/projects/_section.html.haml b/app/views/projects/_section.html.haml
new file mode 100644
index 00000000000..0b7f4cb780a
--- /dev/null
+++ b/app/views/projects/_section.html.haml
@@ -0,0 +1,35 @@
+%ul.nav.nav-tabs
+ %li.active
+ = link_to '#tab-activity', 'data-toggle' => 'tab' do
+ Activity
+ - if @repository.readme
+ %li
+ = link_to '#tab-readme', 'data-toggle' => 'tab' do
+ Readme
+.tab-content
+ .tab-pane.active#tab-activity
+ .hidden-xs
+ = render "events/event_last_push", event: @last_push
+
+ - if current_user
+ %ul.nav.nav-pills.event_filter.pull-right
+ %li
+ = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'rss-btn' do
+ %i.fa.fa-rss
+ Activity Feed
+
+ = render 'shared/event_filter'
+ %hr
+ .content_list
+ = spinner
+
+ - if readme = @repository.readme
+ .tab-pane#tab-readme
+ %article.readme-holder#README
+ .clearfix
+ %small.pull-right
+ = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do
+ %i.fa.fa-file
+ = readme.name
+ .wiki
+ = render_readme(readme)
diff --git a/app/views/projects/_settings_nav.html.haml b/app/views/projects/_settings_nav.html.haml
deleted file mode 100644
index 7fc3d44034f..00000000000
--- a/app/views/projects/_settings_nav.html.haml
+++ /dev/null
@@ -1,31 +0,0 @@
-%ul.project-settings-nav.sidebar-subnav
- = nav_link(path: 'projects#edit') do
- = link_to edit_project_path(@project), title: 'Project', class: "stat-tab tab " do
- %i.fa.fa-pencil-square-o
- %span
- Project
- = nav_link(controller: [:team_members, :teams]) do
- = link_to namespace_project_team_index_path(@project.namespace, @project), title: 'Members', class: "team-tab tab" do
- %i.fa.fa-users
- %span
- Members
- = nav_link(controller: :deploy_keys) do
- = link_to namespace_project_deploy_keys_path(@project.namespace, @project), title: 'Deploy Keys' do
- %i.fa.fa-key
- %span
- Deploy Keys
- = nav_link(controller: :hooks) do
- = link_to namespace_project_hooks_path(@project.namespace, @project), title: 'Web Hooks' do
- %i.fa.fa-link
- %span
- Web Hooks
- = nav_link(controller: :services) do
- = link_to namespace_project_services_path(@project.namespace, @project), title: 'Services' do
- %i.fa.fa-cogs
- %span
- Services
- = nav_link(controller: :protected_branches) do
- = link_to namespace_project_protected_branches_path(@project.namespace, @project), title: 'Protected Branches' do
- %i.fa.fa-lock
- %span
- Protected branches
diff --git a/app/views/projects/_visibility_level.html.haml b/app/views/projects/_visibility_level.html.haml
deleted file mode 100644
index 42c8e685224..00000000000
--- a/app/views/projects/_visibility_level.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'control-label' do
- Visibility Level
- = link_to "(?)", help_page_path("public_access", "public_access")
- .col-sm-10
- - if can_change_visibility_level
- - Gitlab::VisibilityLevel.values.each do |level|
- .radio
- - restricted = restricted_visibility_levels.include?(level)
- = label :project_visibility_level, level do
- = f.radio_button :visibility_level, level, checked: (visibility_level == level), disabled: restricted
- = visibility_level_icon(level)
- .option-title
- = visibility_level_label(level)
- .option-descr
- = visibility_level_description(level)
- - unless restricted_visibility_levels.empty?
- .col-sm-10
- %span.info
- Some visibility level settings have been restricted by the administrator.
- - else
- .col-sm-10
- %span.info
- = visibility_level_icon(visibility_level)
- %strong
- = visibility_level_label(visibility_level)
- .light= visibility_level_description(visibility_level)
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 5a33d18e631..462f5b7afb0 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,17 +1,19 @@
+- page_title "Blame", @blob.path, @ref
%h3.page-title Blame view
#tree-holder.tree-holder
.file-holder
.file-title
%i.fa.fa-file
- %span.file_name
+ %strong
= @path
- %small= number_to_human_size @blob.size
- %span.options= render "projects/blob/actions"
+ %small= number_to_human_size @blob.size
+ .file-actions
+ = render "projects/blob/actions"
.file-content.blame.highlight
%table
- @blame.each do |commit, lines, since|
- - commit = Commit.new(commit)
+ - commit = Commit.new(commit, @project)
%tr
%td.blame-commit
%span.commit
@@ -30,5 +32,5 @@
%code
:erb
<% lines.each do |line| %>
- <%= highlight(@blob.name, line.force_encoding("utf-8"), true).html_safe %>
+ <%= highlight(@blob.name, line, true).html_safe %>
<% end %>
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index b5b29540bb6..13f8271b979 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -1,22 +1,22 @@
.btn-group.tree-btn-group
= edit_blob_link(@project, @ref, @path)
= link_to 'Raw', namespace_project_raw_path(@project.namespace, @project, @id),
- class: 'btn btn-small', target: '_blank'
+ class: 'btn btn-sm', target: '_blank'
-# only show normal/blame view links for text files
- if @blob.text?
- if current_page? namespace_project_blame_path(@project.namespace, @project, @id)
= link_to 'Normal View', namespace_project_blob_path(@project.namespace, @project, @id),
- class: 'btn btn-small'
+ class: 'btn btn-sm'
- else
= link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
- class: 'btn btn-small' unless @blob.empty?
+ class: 'btn btn-sm' unless @blob.empty?
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
- class: 'btn btn-small'
+ class: 'btn btn-sm'
- if @ref != @commit.sha
= link_to 'Permalink', namespace_project_blob_path(@project.namespace, @project,
- tree_join(@commit.sha, @path)), class: 'btn btn-small'
+ tree_join(@commit.sha, @path)), class: 'btn btn-sm'
- if allowed_tree_edit?
- = button_tag class: 'remove-blob btn btn-small btn-remove',
+ = button_tag class: 'remove-blob btn btn-sm btn-remove',
'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do
Remove
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 9ff61f3887f..65c3ab10e02 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -21,12 +21,14 @@
%div#tree-content-holder.tree-content-holder
%article.file-holder
- .file-title.clearfix
- %i.fa.fa-file
- %span.file_name
+ .file-title
+ = blob_icon blob.mode, blob.name
+ %strong
= blob.name
- %small= number_to_human_size blob.size
- %span.options.hidden-xs= render "actions"
+ %small
+ = number_to_human_size(blob.size)
+ .file-actions.hidden-xs
+ = render "actions"
- if blob.text?
= render "text", blob: blob
- elsif blob.image?
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 1f61a0b940c..e78181f8801 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", @blob.path, @ref
.file-editor
%ul.nav.nav-tabs.js-edit-mode
%li.active
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index d78a01f6422..9b1d03b820e 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New File", @ref
%h3.page-title New file
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file') do
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 69167654c39..a1d464bac59 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @blob.path, @ref
%div.tree-ref-holder
= render 'shared/ref_switcher', destination: 'blob', path: @path
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 8de629b03e9..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-small'
+ = 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-small', 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-small 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/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index a313ffcf272..80acc937908 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Branches"
= render "projects/commits/head"
%h3.page-title
Branches
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e5fcb98c68c..cac5dc91afd 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Branch"
- if @error
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 7409f702c5d..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 »
@@ -48,5 +49,4 @@
= preserve(gfm(escape_once(@commit.description)))
:coffeescript
- $ ->
- $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}")
+ $(".commit-info-row.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}")
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index fc721067ed4..fc91f71e8d2 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
= render "commit_box"
= render "projects/diffs/diffs", diffs: @diffs, project: @project
= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 4c853f577e9..083fca9b658 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -12,8 +12,8 @@
- if @note_counts
- note_count = @note_counts.fetch(commit.id, 0)
- else
- - notes = project.notes.for_commit_id(commit.id)
- - note_count = notes.count
+ - notes = commit.notes
+ - note_count = notes.user.count
- if note_count > 0
%span.light
diff --git a/app/views/projects/commits/_commit_list.html.haml b/app/views/projects/commits/_commit_list.html.haml
index 2ee7d73bd20..ce60fbdf032 100644
--- a/app/views/projects/commits/_commit_list.html.haml
+++ b/app/views/projects/commits/_commit_list.html.haml
@@ -3,9 +3,9 @@
Commits (#{@commits.count})
- if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
%ul.well-list
- - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE)).each do |commit|
+ - Commit.decorate(@commits.first(MergeRequestDiff::COMMITS_SAFE_SIZE), @project).each do |commit|
= render "projects/commits/inline_commit", commit: commit, project: @project
%li.warning-row.unstyled
other #{@commits.size - MergeRequestDiff::COMMITS_SAFE_SIZE} commits hidden to prevent performance issues.
- else
- %ul.well-list= render Commit.decorate(@commits), project: @project
+ %ul.well-list= render Commit.decorate(@commits, @project), project: @project
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/commits/show.atom.builder b/app/views/projects/commits/show.atom.builder
index 9211de72b1b..01edd9447ce 100644
--- a/app/views/projects/commits/show.atom.builder
+++ b/app/views/projects/commits/show.atom.builder
@@ -1,18 +1,18 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "Recent commits to #{@project.name}:#{@ref}"
- xml.link :href => namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom), :rel => "self", :type => "application/atom+xml"
- xml.link :href => namespace_project_commits_url(@project.namespace, @project, @ref), :rel => "alternate", :type => "text/html"
+ xml.title "#{@project.name}:#{@ref} commits"
+ xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
+ xml.link href: namespace_project_commits_url(@project.namespace, @project, @ref), rel: "alternate", type: "text/html"
xml.id namespace_project_commits_url(@project.namespace, @project, @ref)
xml.updated @commits.first.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ") if @commits.any?
@commits.each do |commit|
xml.entry do
- xml.id namespace_project_commit_url(@project.namespace, @project, :id => commit.id)
- xml.link :href => namespace_project_commit_url(@project.namespace, @project, :id => commit.id)
- xml.title truncate(commit.title, :length => 80)
+ xml.id namespace_project_commit_url(@project.namespace, @project, id: commit.id)
+ xml.link href: namespace_project_commit_url(@project.namespace, @project, id: commit.id)
+ xml.title truncate(commit.title, length: 80)
xml.updated commit.committed_date.strftime("%Y-%m-%dT%H:%M:%SZ")
- xml.media :thumbnail, :width => "40", :height => "40", :url => avatar_icon(commit.author_email)
+ xml.media :thumbnail, width: "40", height: "40", url: avatar_icon(commit.author_email)
xml.author do |author|
xml.name commit.author_name
xml.email commit.author_email
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 7ea855e1a4e..c8531b090a6 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,3 +1,8 @@
+- page_title "Commits", @ref
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
+
= render "head"
.tree-ref-holder
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index 4745bfbeaaf..d1e579a2ede 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Compare"
= render "projects/commits/head"
%h3.page-title
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 214b5bd337b..3670dd5c13b 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "#{params[:from]}...#{params[:to]}"
= render "projects/commits/head"
%h3.page-title
diff --git a/app/views/projects/deploy_keys/_deploy_key.html.haml b/app/views/projects/deploy_keys/_deploy_key.html.haml
index 230e164f24c..c577dfa8d55 100644
--- a/app/views/projects/deploy_keys/_deploy_key.html.haml
+++ b/app/views/projects/deploy_keys/_deploy_key.html.haml
@@ -1,25 +1,36 @@
%li
.pull-right
- if @available_keys.include?(deploy_key)
- = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-small', method: :put do
+ = link_to enable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-sm', method: :put do
%i.fa.fa-plus
Enable
- else
- - if deploy_key.projects.count > 1
- = link_to disable_namespace_project_deploy_key_path(@project.namespace, @project, deploy_key), class: 'btn btn-small', method: :put do
+ - 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-small 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..2e9c5dc08c8 100644
--- a/app/views/projects/deploy_keys/index.html.haml
+++ b/app/views/projects/deploy_keys/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Deploy Keys"
%h3.page-title
Deploy keys allow read-only access to the repository
@@ -22,11 +23,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/deploy_keys/new.html.haml b/app/views/projects/deploy_keys/new.html.haml
index 186d6b58972..01c810aee18 100644
--- a/app/views/projects/deploy_keys/new.html.haml
+++ b/app/views/projects/deploy_keys/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Deploy Key"
%h3.page-title New Deploy key
%hr
diff --git a/app/views/projects/deploy_keys/show.html.haml b/app/views/projects/deploy_keys/show.html.haml
index 405b5bcd0d3..7d44652af72 100644
--- a/app/views/projects/deploy_keys/show.html.haml
+++ b/app/views/projects/deploy_keys/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @key.title, "Deploy Keys"
%h3.page-title
Deploy key:
= @key.title
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 48d4c33ce85..ec8974c5475 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,16 +1,17 @@
-.row
- .col-md-8
- = render 'projects/diffs/stats', diffs: diffs
- .col-md-4
- .btn-group.pull-right
+.prepend-top-20.append-bottom-20
+ .pull-right
+ .btn-group
= inline_diff_btn
= parallel_diff_btn
+ = render 'projects/diffs/stats', diffs: diffs
-- if show_diff_size_warning?(diffs)
- = render 'projects/diffs/warning', diffs: diffs
+- diff_files = safe_diff_files(diffs)
+
+- if diff_files.count < diffs.size
+ = render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
.files
- - safe_diff_files(diffs).each_with_index do |diff_file, index|
+ - diff_files.each_with_index do |diff_file, index|
= render 'projects/diffs/file', diff_file: diff_file, i: index, project: project
- if @diff_timeout
@@ -19,3 +20,6 @@
Failed to collect changes
%p
Maybe diff is really big and operation failed with timeout. Try to get diff locally
+
+:coffeescript
+ $('.files .diff-header').stick_in_parent(offset_top: $('.navbar').height())
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 2569e91ccfa..d4b019780f5 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -11,25 +11,20 @@
= view_file_btn(@commit.parent_id, diff_file, project)
- elsif diff_file.diff.submodule?
- submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
- = submodule_link(submodule_item, @commit.id)
+ = submodule_link(submodule_item, @commit.id, project.repository)
- else
- - if diff_file.renamed_file
- %span= "#{diff_file.old_path} renamed to #{diff_file.new_path}"
- - else
- %span= diff_file.new_path
- - if diff_file.mode_changed?
- %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
+ %span
+ - if diff_file.renamed_file
+ = "#{diff_file.old_path} renamed to #{diff_file.new_path}"
+ - else
+ = diff_file.new_path
+ - if diff_file.mode_changed?
+ %span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
.diff-btn-group
- if blob.text?
- - unless params[:view] == 'parallel'
- %label
- = check_box_tag nil, 1, false, class: 'js-toggle-diff-line-wrap'
- Wrap text
- &nbsp;
- = link_to '#', class: 'js-toggle-diff-comments btn btn-small' do
- %i.fa.fa-chevron-down
- Show/Hide comments
+ = link_to '#', class: 'js-toggle-diff-comments btn btn-sm active has_tooltip', title: "Toggle comments for this file" do
+ %i.fa.fa-comments
&nbsp;
- if @merge_request && @merge_request.source_project
@@ -39,7 +34,7 @@
= view_file_btn(@commit.id, diff_file, project)
- .diff-content
+ .diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
- return unless blob.respond_to?('text?')
- if blob.text?
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 9b5eb84a86d..1625930615a 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -1,17 +1,14 @@
.js-toggle-container
.commit-stat-summary
Showing
- %strong.cdark #{pluralize(diffs.count, "changed file")}
+ = link_to '#', class: 'js-toggle-button' do
+ %strong #{pluralize(diffs.count, "changed file")}
- if current_controller?(:commit)
- unless @commit.has_zero_stats?
with
%strong.cgreen #{@commit.stats.additions} additions
and
%strong.cred #{@commit.stats.deletions} deletions
- &nbsp;
- = link_to '#', class: 'btn btn-small js-toggle-button' do
- Show diff stats
- %i.fa.fa-chevron-down
.file-stats.js-toggle-content.hide
%ul.bordered-list
- diffs.each_with_index do |diff, i|
diff --git a/app/views/projects/diffs/_text_file.html.haml b/app/views/projects/diffs/_text_file.html.haml
index b1c987563f1..e6dfbfd6511 100644
--- a/app/views/projects/diffs/_text_file.html.haml
+++ b/app/views/projects/diffs/_text_file.html.haml
@@ -16,14 +16,14 @@
- else
%td.old_line
= link_to raw(type == "new" ? "&nbsp;" : line_old), "##{line_code}", id: line_code
- - if @comments_allowed
+ - if @comments_allowed && can?(current_user, :write_note, @project)
= link_to_new_diff_note(line_code)
%td.new_line{data: {linenumber: line.new_pos}}
= link_to raw(type == "old" ? "&nbsp;" : line.new_pos) , "##{line_code}", id: line_code
%td.line_content{class: "noteable_line #{type} #{line_code}", "line_code" => line_code}= raw diff_line_content(line.text)
- 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/diffs/_warning.html.haml b/app/views/projects/diffs/_warning.html.haml
index af1f342afbd..bd0b7376ba7 100644
--- a/app/views/projects/diffs/_warning.html.haml
+++ b/app/views/projects/diffs/_warning.html.haml
@@ -3,17 +3,17 @@
Too many changes.
.pull-right
- unless diff_hard_limit_enabled?
- = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-small btn-warning"
+ = link_to "Reload with full diff", url_for(params.merge(force_show_diff: true)), class: "btn btn-sm btn-warning"
- if current_controller?(:commit) or current_controller?(:merge_requests)
- if current_controller?(:commit)
- = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-small"
- = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-small"
+ = link_to "Plain diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff), class: "btn btn-warning btn-sm"
+ = link_to "Email patch", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch), class: "btn btn-warning btn-sm"
- elsif @merge_request && @merge_request.persisted?
- = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-small"
- = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-small"
+ = link_to "Plain diff", merge_request_path(@merge_request, format: :diff), class: "btn btn-warning btn-sm"
+ = link_to "Email patch", merge_request_path(@merge_request, format: :patch), class: "btn btn-warning btn-sm"
%p
To preserve performance only
- %strong #{allowed_diff_size} of #{diffs.size}
+ %strong #{shown_files_count} of #{diffs.size}
files are displayed.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b4c36beda88..c09d794ef7f 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -29,16 +29,13 @@
.col-sm-10= f.select(:default_branch, @repository.branch_names, {}, {class: 'select2 select-wide'})
- = render "visibility_level", f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project)
+ = render 'shared/visibility_level', f: f, visibility_level: @project.visibility_level, can_change_visibility_level: can?(current_user, :change_visibility_level, @project), form_model: @project
- %fieldset.features
- %legend
- Tags:
- .form-group
- = f.label :tag_list, "Tags", class: 'control-label'
- .col-sm-10
- = f.text_field :tag_list, maxlength: 2000, class: "form-control"
- %p.hint Separate tags with commas.
+ .form-group
+ = f.label :tag_list, "Tags", class: 'control-label'
+ .col-sm-10
+ = f.text_field :tag_list, maxlength: 2000, class: "form-control"
+ %p.help-block Separate tags with commas.
%fieldset.features
%legend
@@ -87,7 +84,7 @@
You can change your project avatar here
- else
You can upload a project avatar here
- %a.choose-btn.btn.btn-small.js-choose-project-avatar-button
+ %a.choose-btn.btn.btn-sm.js-choose-project-avatar-button
%i.icon-paper-clip
%span Choose File ...
&nbsp;
@@ -96,7 +93,7 @@
.light The maximum file size allowed is 200KB.
- if @project.avatar?
%hr
- = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-small remove-avatar"
+ = link_to 'Remove avatar', namespace_project_avatar_path(@project.namespace, @project), data: { confirm: "Project avatar will be removed. Are you sure?"}, method: :delete, class: "btn btn-remove btn-sm remove-avatar"
.form-actions
= f.submit 'Save changes', class: "btn btn-save"
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 49806ceaa96..8080a904978 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -13,6 +13,8 @@
add a file
&nbsp;or do a push via the command line.
+.well
+ = render "shared/clone_panel"
%h4
%strong Command line instructions
%div.git-empty
@@ -27,20 +29,19 @@
%legend Create a new repository
%pre.dark
:preserve
- mkdir #{@project.path}
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
cd #{@project.path}
- git init
touch README.md
git add README.md
- git commit -m "first commit"
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git commit -m "add README"
git push -u origin master
%fieldset
- %legend Push an existing Git repository
+ %legend Existing folder or Git repository
%pre.dark
:preserve
- cd existing_git_repo
+ cd existing_folder
+ git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git push -u origin master
diff --git a/app/views/projects/forks/error.html.haml b/app/views/projects/forks/error.html.haml
index 8eb4f795971..3d0ab5b85d6 100644
--- a/app/views/projects/forks/error.html.haml
+++ b/app/views/projects/forks/error.html.haml
@@ -1,3 +1,4 @@
+- page_title "Fork project"
- if @forked_project && !@forked_project.saved?
.alert.alert-danger.alert-block
%h4
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index 5a6c46f3208..b7a2ed68e25 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "Fork project"
%h3.page-title Fork project
%p.lead
Click to fork the project to a user or group
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index 4a5d09b9503..254a76e108b 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,3 +1,4 @@
+- page_title "Commit statistics"
= render 'head'
%p.lead
@@ -54,7 +55,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 +69,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 +83,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/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index e3d5094ddc5..3a8dc89f84c 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Contributor statistics"
= render 'head'
.loading-graph
.center
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index e70cf5c3884..808c03148f4 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Web Hooks"
%h3.page-title
Web hooks
@@ -58,8 +59,8 @@
- @hooks.each do |hook|
%li
.pull-right
- = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-small btn-grouped"
- = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-small btn-grouped"
+ = link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
.clearfix
%span.monospace= hook.url
%p
diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml
index f1248ac2af5..f8f2e192e29 100644
--- a/app/views/projects/imports/new.html.haml
+++ b/app/views/projects/imports/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "Import repository"
%h3.page-title
- if @project.import_failed?
Import failed. Retry?
@@ -12,7 +13,7 @@
%span Import existing git repo
.col-sm-10
= f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git'
- .alert.alert-info
+ .well.prepend-top-20
This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git.
%br
The import will time out after 4 minutes. For big repositories, use a clone/push combination.
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 2d1fdafed24..39fe0fc1c4f 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Import in progress"
.save-project-loader
.center
%h2
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index fc3e35640dc..2016f5c709c 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,37 +1,34 @@
- content_for :note_actions do
- if can?(current_user, :modify_issue, @issue)
- if @issue.closed?
- = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen js-note-target-reopen", title: 'Reopen Issue'
+ = link_to 'Reopen Issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen js-note-target-reopen', title: 'Reopen Issue'
- else
- = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close js-note-target-close", title: "Close Issue"
+ = link_to 'Close Issue', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close js-note-target-close', title: 'Close Issue'
+
+= render 'shared/show_aside'
+
.row
%section.col-md-9
+ .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"
+ .voting_notes#notes= render 'projects/notes/notes_with_form'
%aside.col-md-3
.issuable-affix
.clearfix
- %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
+ %span.slead.has_tooltip{title: 'Cross-project reference'}
= cross_project_reference(@project, @issue)
%hr
.context
= render partial: 'issue_context', locals: { issue: @issue }
- %hr
- .clearfix
- .votes-holder
- %h6 Votes
- #votes= render 'votes/votes_block', votable: @issue
- if @issue.labels.any?
- %hr
- %h6 Labels
+ .issuable-context-title
+ %label Labels
.issue-show-labels
- @issue.labels.each do |label|
= link_to namespace_project_issues_path(@project.namespace, @project, label_name: label.name) do
= render_colored_label(label)
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 01e2133e283..ef36d1f9547 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -6,23 +6,34 @@
.issue-title
%span.str-truncated
= link_to_gfm issue.title, issue_path(issue), class: "row_title"
+ .issue-labels
+ - issue.labels.each do |label|
+ = link_to namespace_project_issues_path(issue.project.namespace, issue.project, label_name: label.name) do
+ = render_colored_label(label)
.pull-right.light
- if issue.closed?
%span
CLOSED
- - if issue.notes.any?
+ - if issue.assignee
+ = link_to_member(@project, issue.assignee, name: false)
+ - note_count = issue.notes.user.count
+ - if note_count > 0
&nbsp;
%span
%i.fa.fa-comments
- = issue.notes.count
+ = note_count
+ - else
+ &nbsp;
+ %span.issue-no-comments
+ %i.fa.fa-comments
+ = 0
.issue-info
- %span.light= "##{issue.iid}"
- - if issue.assignee
- assigned to #{link_to_member(@project, issue.assignee)}
+ = "##{issue.iid} opened #{time_ago_with_tooltip(issue.created_at, 'bottom')} by #{link_to_member(@project, issue.author, avatar: false)}".html_safe
- if issue.votes_count > 0
= render 'votes/votes_inline', votable: issue
- if issue.milestone
+ &nbsp;
%span
%i.fa.fa-clock-o
= issue.milestone.title
@@ -32,20 +43,3 @@
.pull-right.issue-updated-at
%small updated #{time_ago_with_tooltip(issue.updated_at, 'bottom', 'issue_update_ago')}
-
- .issue-labels
- - issue.labels.each do |label|
- = link_to namespace_project_issues_path(issue.project.namespace, issue.project, label_name: label.name) do
- = render_colored_label(label)
-
- .issue-actions
- - if can? current_user, :modify_issue, issue
- - if issue.closed?
- = link_to 'Reopen', issue_path(issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-small btn-grouped reopen_issue btn-reopen", remote: true
- - else
- = link_to 'Close', issue_path(issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-small btn-grouped close_issue btn-close", remote: true
- = link_to edit_namespace_project_issue_path(issue.project.namespace, issue.project, issue), class: "btn btn-small edit-issue-link btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
-
-
diff --git a/app/views/projects/issues/_issue_context.html.haml b/app/views/projects/issues/_issue_context.html.haml
index 4c7654354f4..323f5c84a85 100644
--- a/app/views/projects/issues/_issue_context.html.haml
+++ b/app/views/projects/issues/_issue_context.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @issue], remote: true, html: {class: 'edit-issue inline-update js-issue-update'} do |f|
%div.prepend-top-20
.issuable-context-title
%label
@@ -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
@@ -26,3 +26,21 @@
= f.select(:milestone_id, milestone_options(@issue), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'})
= hidden_field_tag :issue_context
= f.submit class: 'btn'
+
+ - if current_user
+ %div.prepend-top-20.clearfix
+ .issuable-context-title
+ %label
+ Subscription:
+ %button.btn.btn-block.subscribe-button{:type => 'button'}
+ %i.fa.fa-eye
+ %span= @issue.subscribed?(current_user) ? "Unsubscribe" : "Subscribe"
+ - subscribtion_status = @issue.subscribed?(current_user) ? "subscribed" : "unsubscribed"
+ .subscription-status{"data-status" => subscribtion_status}
+ .description-block.unsubscribed{class: ( "hidden" if @issue.subscribed?(current_user) )}
+ You're not receiving notifications from this thread.
+ .description-block.subscribed{class: ( "hidden" unless @issue.subscribed?(current_user) )}
+ You're receiving notifications because you're subscribed to this thread.
+
+:coffeescript
+ new Subscription("#{toggle_subscription_namespace_project_issue_path(@issue.project.namespace, @project, @issue)}")
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index b1bc3ba0eba..53b6f0879c9 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1 +1,2 @@
+- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
= render "form"
diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder
index 126f2c07faa..5fa8fbdf893 100644
--- a/app/views/projects/issues/index.atom.builder
+++ b/app/views/projects/issues/index.atom.builder
@@ -1,8 +1,8 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "#{@project.name} issues"
- xml.link :href => namespace_project_issues_url(@project.namespace, @project, :atom), :rel => "self", :type => "application/atom+xml"
- xml.link :href => namespace_project_issues_url(@project.namespace, @project), :rel => "alternate", :type => "text/html"
+ xml.link href: namespace_project_issues_url(@project.namespace, @project, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
+ xml.link href: namespace_project_issues_url(@project.namespace, @project), rel: "alternate", type: "text/html"
xml.id namespace_project_issues_url(@project.namespace, @project)
xml.updated @issues.first.created_at.strftime("%Y-%m-%dT%H:%M:%SZ") if @issues.any?
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index cbbcb1d06c0..709ea1f7897 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,3 +1,8 @@
+- page_title "Issues"
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
+
.append-bottom-10
.pull-right
.pull-left
@@ -6,14 +11,7 @@
= link_to namespace_project_issues_path(@project.namespace, @project, :atom, { private_token: current_user.private_token }), class: 'btn append-right-10' do
%i.fa.fa-rss
- = form_tag namespace_project_issues_path(@project.namespace, @project), method: :get, id: "issue_search_form", class: 'pull-left issue-search-form' do
- .append-right-10.hidden-xs.hidden-sm
- = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' }
- = hidden_field_tag :state, params['state']
- = hidden_field_tag :scope, params['scope']
- = hidden_field_tag :assignee_id, params['assignee_id']
- = hidden_field_tag :milestone_id, params['milestone_id']
- = hidden_field_tag :label_id, params['label_id']
+ = render 'shared/issuable_search_form', path: namespace_project_issues_path(@project.namespace, @project)
- if can? current_user, :write_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { assignee_id: params[:assignee_id], milestone_id: params[:milestone_id]}), class: "btn btn-new pull-left", title: "New Issue", id: "new_issue_link" do
@@ -22,15 +20,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")
- = 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/new.html.haml b/app/views/projects/issues/new.html.haml
index b1bc3ba0eba..da6edd5c2d2 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1 +1,2 @@
+- page_title "New Issue"
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bd28d8a1db2..ee1b2a08bc4 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
.issue
.issue-details
%h4.page-title
@@ -12,17 +13,17 @@
.pull-right
- if can?(current_user, :write_issue, @project)
- = link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-grouped new-issue-link", title: "New Issue", id: "new_issue_link" do
- %i.fa.fa-plus
+ = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'btn btn-grouped new-issue-link', title: 'New Issue', id: 'new_issue_link' do
+ = icon('plus')
New Issue
- if can?(current_user, :modify_issue, @issue)
- if @issue.closed?
- = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen }, status_only: true), method: :put, class: "btn btn-grouped btn-reopen"
+ = link_to 'Reopen', issue_path(@issue, issue: {state_event: :reopen}, status_only: true), method: :put, class: 'btn btn-grouped btn-reopen'
- else
- = link_to 'Close', issue_path(@issue, issue: {state_event: :close }, status_only: true), method: :put, class: "btn btn-grouped btn-close", title: "Close Issue"
+ = link_to 'Close', issue_path(@issue, issue: {state_event: :close}, status_only: true), method: :put, class: 'btn btn-grouped btn-close', title: 'Close Issue'
- = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: "btn btn-grouped issuable-edit" do
- %i.fa.fa-pencil-square-o
+ = link_to edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'btn btn-grouped issuable-edit' do
+ = icon('pencil-square-o')
Edit
%hr
@@ -30,11 +31,13 @@
= gfm escape_once(@issue.title)
%div
- if @issue.description.present?
- .description
+ .description{class: can?(current_user, :modify_issue, @issue) ? 'js-task-list-container' : ''}
.wiki
= preserve do
- = markdown(@issue.description, parse_tasks: true)
+ = markdown(@issue.description)
+ %textarea.hidden.js-task-list-field
+ = @issue.description
%hr
.issue-discussion
- = render "projects/issues/discussion"
+ = render 'projects/issues/discussion'
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/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index 2305fce112e..261d52dedc1 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -16,9 +16,9 @@
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
- = f.color_field :color, placeholder: "#AA33EE", class: "form-control"
+ = f.color_field :color, class: "form-control"
.help-block
- 6 character hex values starting with a # sign.
+ Choose any color.
%br
Or you can choose one of suggested colors below
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index e003d1dfe7f..645402667fd 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", @label.name, "Labels"
%h3
Edit label
%span.light #{@label.name}
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 0700e72d39c..7d19415a7f4 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Labels"
- if can? current_user, :admin_label, @project
= link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
New label
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index 0683ed5d4fb..b3ef17025c3 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Label"
%h3 New label
.back-link
= link_to namespace_project_labels_path(@project.namespace, @project) do
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 79a093dc775..9a2aa9c3de0 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -5,8 +5,12 @@
- if @merge_request.closed?
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-grouped btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request"
+= render 'shared/show_aside'
+
.row
%section.col-md-9
+ .votes-holder.pull-right
+ #votes= render 'votes/votes_block', votable: @merge_request
= render "projects/merge_requests/show/participants"
= render "projects/notes/notes_with_form"
%aside.col-md-3
@@ -17,17 +21,11 @@
%hr
.context
= render partial: 'projects/merge_requests/show/context', locals: { merge_request: @merge_request }
- %hr
- .votes-holder
- %h6 Votes
- #votes= render 'votes/votes_block', votable: @merge_request
- if @merge_request.labels.any?
- %hr
- %h6 Labels
+ .issuable-context-title
+ %label Labels
.merge-request-show-labels
- @merge_request.labels.each do |label|
= link_to namespace_project_merge_requests_path(@project.namespace, @project, label_name: label.name) do
= render_colored_label(label)
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 1eba1a96b7b..5d5a23b5409 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -2,6 +2,10 @@
.merge-request-title
%span.str-truncated
= link_to_gfm merge_request.title, merge_request_path(merge_request), class: "row_title"
+ .merge-request-labels
+ - merge_request.labels.each do |label|
+ = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, label_name: label.name) do
+ = render_colored_label(label)
.pull-right.light
- if merge_request.merged?
%span
@@ -16,20 +20,27 @@
%span.label-branch<
%i.fa.fa-code-fork
%span= merge_request.target_branch
- - if merge_request.notes.any?
+ - note_count = merge_request.mr_and_commit_notes.user.count
+ - if merge_request.assignee
+ &nbsp;
+ = link_to_member(merge_request.source_project, merge_request.assignee, name: false)
+ - if note_count > 0
&nbsp;
%span
%i.fa.fa-comments
- = merge_request.mr_and_commit_notes.count
+ = note_count
+ - else
+ &nbsp;
+ %span.merge-request-no-comments
+ %i.fa.fa-comments
+ = 0
+
.merge-request-info
- %span.light= "##{merge_request.iid}"
- - if merge_request.assignee
- assigned to #{link_to_member(merge_request.source_project, merge_request.assignee)}
- - else
- Unassigned
+ = "##{merge_request.iid} opened #{time_ago_with_tooltip(merge_request.created_at, 'bottom')} by #{link_to_member(@project, merge_request.author, avatar: false)}".html_safe
- if merge_request.votes_count > 0
= render 'votes/votes_inline', votable: merge_request
- if merge_request.milestone_id?
+ &nbsp;
%span
%i.fa.fa-clock-o
= merge_request.milestone.title
@@ -37,11 +48,5 @@
%span.task-status
= merge_request.task_status
-
.pull-right.hidden-xs
%small updated #{time_ago_with_tooltip(merge_request.updated_at, 'bottom', 'merge_request_updated_ago')}
-
- .merge-request-labels
- - merge_request.labels.each do |label|
- = link_to namespace_project_merge_requests_path(merge_request.project.namespace, merge_request.project, label_name: label.name) do
- = render_colored_label(label)
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
new file mode 100644
index 00000000000..b8a0ca9a42f
--- /dev/null
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -0,0 +1,13 @@
+.panel.panel-default
+ %ul.well-list.mr-list
+ = render @merge_requests
+ - if @merge_requests.blank?
+ %li
+ .nothing-here-block No merge requests to show
+
+- if @merge_requests.present?
+ .pull-right
+ %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter
+
+ = paginate @merge_requests, theme: "gitlab"
+
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 17e76059fdb..e611b23bca6 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -1,5 +1,4 @@
-%h3.page-title Compare branches for new Merge Request
-%hr
+%p.lead Compare branches for new Merge Request
= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], url: new_namespace_project_merge_request_path(@project.namespace, @project), method: :get, html: { class: "merge-request-form form-inline" } do |f|
.hide.alert.alert-danger.mr-compare-errors
@@ -52,8 +51,8 @@
are the same.
- %hr
- = f.submit 'Compare branches', class: "btn btn-primary mr-compare-btn"
+ %div
+ = f.submit 'Compare branches', class: "btn btn-new mr-compare-btn"
:javascript
var source_branch = $("#merge_request_source_branch")
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index bf80afe8785..24a9563dd4d 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -7,87 +7,24 @@
%strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
%span.pull-right
- = link_to 'Change branches', new_namespace_project_merge_request_path(@project.namespace, @project)
-
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: "merge-request-form form-horizontal gfm-form" } do |f|
+ = link_to 'Change branches', mr_change_branches_path(@merge_request)
+%hr
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form form-horizontal gfm-form' } do |f|
.merge-request-form-info
- .form-group
- = f.label :title, class: 'control-label' do
- %strong Title *
- .col-sm-10
- = f.text_field :title, maxlength: 255, autofocus: true, class: 'form-control pad js-gfm-input', required: true
- .form-group.issuable-description
- = f.label :description, 'Description', class: 'control-label'
- .col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do
- = render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
-
- .col-sm-12-hint
- .pull-left
- Parsed with
- #{link_to 'Gitlab Flavored Markdown', help_page_path('markdown', 'markdown'), target: '_blank'}.
- .pull-right
- Attach files by dragging &amp; dropping
- or #{link_to 'selecting them', '#', class: 'markdown-selector'}.
-
- .clearfix
- .error-alert
- %hr
- .form-group
- .issue-assignee
- = f.label :assignee_id, class: 'control-label' do
- %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)
- &nbsp;
- = link_to 'Assign to me', '#', class: 'btn assign-to-me-link'
- .form-group
- .issue-milestone
- = f.label :milestone_id, class: 'control-label' do
- %i.fa.fa-clock-o
- Milestone
- .col-sm-10
- - if milestone_options(@merge_request).present?
- = f.select(:milestone_id, milestone_options(@merge_request), {include_blank: 'Select milestone'}, {class: 'select2'})
- - else
- %span.light No open milestones available.
- &nbsp;
- - if can? current_user, :admin_milestone, @merge_request.target_project
- = link_to 'Create new milestone', new_namespace_project_milestone_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank
- .form-group
- = f.label :label_ids, class: 'control-label' do
- %i.fa.fa-tag
- Labels
- .col-sm-10
- - if @merge_request.target_project.labels.any?
- = f.collection_select :label_ids, @merge_request.target_project.labels.all, :id, :name, {selected: @merge_request.label_ids}, multiple: true, class: 'select2'
- - else
- %span.light No labels yet.
- &nbsp;
- - if can? current_user, :admin_label, @merge_request.target_project
- = link_to 'Create new label', new_namespace_project_label_path(@merge_request.target_project.namespace, @merge_request.target_project), target: :blank
-
- .form-actions
- - if contribution_guide_url(@target_project)
- %p
- Please review the
- %strong #{link_to 'guidelines for contribution', contribution_guide_url(@target_project)}
- to this repository.
- = f.hidden_field :source_project_id
- = f.hidden_field :source_branch
- = f.hidden_field :target_project_id
- = f.hidden_field :target_branch
- = f.submit 'Submit merge request', class: 'btn btn-create'
+ = render 'projects/issuable_form', f: f, issuable: @merge_request
+ = f.hidden_field :source_project_id
+ = f.hidden_field :source_branch
+ = f.hidden_field :target_project_id
+ = f.hidden_field :target_branch
.mr-compare.merge-request
%ul.nav.nav-tabs.merge-request-tabs
- %li.commits-tab{data: {action: 'commits'}}
+ %li.commits-tab{data: {action: 'commits', toggle: 'tab'}}
= link_to url_for(params) 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 url_for(params) do
%i.fa.fa-list-alt
Changes
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ca4ceecb225..c2f5cdacae7 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,3 +1,4 @@
+- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
.merge-request{'data-url' => merge_request_path(@merge_request)}
.merge-request-details
= render "projects/merge_requests/show/mr_title"
@@ -36,17 +37,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.count
- %li.commits-tab{data: {action: 'commits'}}
+ %span.badge= @merge_request.mr_and_commit_notes.user.count
+ %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
@@ -72,6 +73,6 @@
check_enable: #{@merge_request.unchecked? ? "true" : "false"},
url_to_ci_check: "#{ci_status_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
ci_enable: #{@project.ci_service ? "true" : "false"},
- current_status: "#{@merge_request.merge_status_name}",
+ current_status: "#{@merge_request.automerge_status}",
action: "#{controller.action_name}"
});
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 839c63986ab..7e5cb07f249 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
%h3.page-title
= "Edit merge request ##{@merge_request.iid}"
%hr
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index e3b9a28033b..ab845a7e719 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,22 +1,12 @@
-.merge-requests-holder
- .append-bottom-10
- .pull-right
- - if can? current_user, :write_merge_request, @project
- = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do
- %i.fa.fa-plus
- New Merge Request
- = render 'shared/issuable_filter'
- .panel.panel-default
- %ul.well-list.mr-list
- = render @merge_requests
- - if @merge_requests.blank?
- %li
- .nothing-here-block No merge requests to show
- - if @merge_requests.present?
- .pull-right
- %span.cgray.pull-right #{@merge_requests.total_count} merge requests for this filter
-
- = paginate @merge_requests, theme: "gitlab"
+- page_title "Merge Requests"
+.append-bottom-10
+ .pull-right
+ = render 'shared/issuable_search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
-:javascript
- $(merge_requestsPage);
+ - if can? current_user, :write_merge_request, @project
+ = link_to new_namespace_project_merge_request_path(@project.namespace, @project), class: "btn btn-new pull-left", title: "New Merge Request" do
+ %i.fa.fa-plus
+ New Merge Request
+ = render 'shared/issuable_filter'
+.merge-requests-holder
+ = render 'merge_requests'
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index b9c466657de..15bd4e2fafd 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -1,3 +1,4 @@
+- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
.merge-request
= render "projects/merge_requests/show/mr_title"
= render "projects/merge_requests/show/mr_box"
diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml
index 4756903d0e0..b038a640f67 100644
--- a/app/views/projects/merge_requests/new.html.haml
+++ b/app/views/projects/merge_requests/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Merge Request"
- if @merge_request.can_be_created
= render 'new_submit'
- else
diff --git a/app/views/projects/merge_requests/show/_context.html.haml b/app/views/projects/merge_requests/show/_context.html.haml
index a74f3fb24e7..a5a821c1847 100644
--- a/app/views/projects/merge_requests/show/_context.html.haml
+++ b/app/views/projects/merge_requests/show/_context.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update'} do |f|
+= form_for [@project.namespace.becomes(Namespace), @project, @merge_request], remote: true, html: {class: 'edit-merge_request inline-update js-merge-request-update'} do |f|
%div.prepend-top-20
.issuable-context-title
%label
@@ -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
@@ -19,12 +19,30 @@
%span.back-to-milestone
= link_to namespace_project_milestone_path(@project.namespace, @project, @merge_request.milestone) do
%strong
- %i.fa.fa-clock-o
+ = icon('clock-o')
= @merge_request.milestone.title
- else
none
.issuable-context-selectbox
- if can?(current_user, :modify_merge_request, @merge_request)
- = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: "Select milestone" }, {class: 'select2 select2-compact js-select2 js-milestone'})
+ = f.select(:milestone_id, milestone_options(@merge_request), { include_blank: 'Select milestone' }, {class: 'select2 select2-compact js-select2 js-milestone'})
= hidden_field_tag :merge_request_context
= f.submit class: 'btn'
+
+ - if current_user
+ %div.prepend-top-20.clearfix
+ .issuable-context-title
+ %label
+ Subscription:
+ %button.btn.btn-block.subscribe-button{:type => 'button'}
+ = icon('eye')
+ %span= @merge_request.subscribed?(current_user) ? 'Unsubscribe' : 'Subscribe'
+ - subscribtion_status = @merge_request.subscribed?(current_user) ? 'subscribed' : 'unsubscribed'
+ .subscription-status{data: {status: subscribtion_status}}
+ .description-block.unsubscribed{class: ( 'hidden' if @merge_request.subscribed?(current_user) )}
+ You're not receiving notifications from this thread.
+ .description-block.subscribed{class: ( 'hidden' unless @merge_request.subscribed?(current_user) )}
+ You're receiving notifications because you're subscribed to this thread.
+
+:coffeescript
+ new Subscription("#{toggle_subscription_namespace_project_merge_request_path(@merge_request.project.namespace, @project, @merge_request)}")
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 63db4b30968..6474d32ac08 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -40,7 +40,6 @@
git merge --no-ff #{@merge_request.source_branch}
git push origin #{@merge_request.target_branch}
-
:javascript
$(function(){
var modal = $('#modal_merge_info').modal({modal: true, show:false});
diff --git a/app/views/projects/merge_requests/show/_mr_accept.html.haml b/app/views/projects/merge_requests/show/_mr_accept.html.haml
index fb2c3220b8a..cb536214c69 100644
--- a/app/views/projects/merge_requests/show/_mr_accept.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_accept.html.haml
@@ -4,9 +4,11 @@
%strong Archived projects cannot be committed to!
- else
.automerge_widget.cannot_be_merged.hide
- %strong This can't be merged automatically, even if it could be merged you don't have the permission to do so.
+ %strong This request can't be merged automatically. Even if it could be merged, you don't have permission to do so.
+ .automerge_widget.work_in_progress.hide
+ %strong This request can't be merged automatically because it is marked a Work In Progress. Even if it could be merged, you don't have permission to do so.
.automerge_widget.can_be_merged.hide
- %strong This can be merged automatically but you don't have the permission to do so.
+ %strong This request can be merged automatically, but you don't have permission to do so.
- if @show_merge_controls
@@ -30,7 +32,7 @@
text: @merge_request.merge_commit_message,
rows: 14, hint: true
- %hr
+ %br
.light
If you still want to merge this request manually - use
%strong
@@ -51,12 +53,24 @@
command line
%p
- %button.btn.disabled
+ %button.btn.disabled{:type => 'button'}
%i.fa.fa-warning
Accept Merge Request
&nbsp;
This usually happens when git can not resolve conflicts between branches automatically.
+ .automerge_widget.work_in_progress.hide
+ %h4
+ This request can't be merged because it is marked a <strong>Work In Progress</strong>.
+
+ %p
+ %button.btn.disabled{:type => 'button'}
+ %i.fa.fa-warning
+ Accept Merge Request
+ &nbsp;
+
+ When the merge request is ready, remove the "WIP" prefix from the title to allow it to be merged.
+
.automerge_widget.unchecked
%p
%strong
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ada9ae58b8f..b3470ba37d6 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -3,7 +3,9 @@
%div
- if @merge_request.description.present?
- .description
+ .description{class: can?(current_user, :modify_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki
= preserve do
- = markdown(@merge_request.description, parse_tasks: true)
+ = markdown(@merge_request.description)
+ %textarea.hidden.js-task-list-field
+ = @merge_request.description
diff --git a/app/views/projects/merge_requests/show/_mr_ci.html.haml b/app/views/projects/merge_requests/show/_mr_ci.html.haml
index 85a7103f3bc..ffa3f7b0e36 100644
--- a/app/views/projects/merge_requests/show/_mr_ci.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_ci.html.haml
@@ -23,6 +23,12 @@
%i.fa.fa-spinner
Checking for CI status for #{@merge_request.last_commit_short_sha}
+ .ci_widget.ci-canceled{style: "display:none"}
+ %i.fa.fa-times
+ %span CI build canceled
+ for #{@merge_request.last_commit_short_sha}.
+ = link_to "View build page", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
+
.ci_widget.ci-error{style: "display:none"}
%i.fa.fa-times
%span Cannot connect to the CI server. Please check your settings and try again.
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/_remove_source_branch.html.haml b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml
index 0a642b7e6d0..59cb85edfce 100644
--- a/app/views/projects/merge_requests/show/_remove_source_branch.html.haml
+++ b/app/views/projects/merge_requests/show/_remove_source_branch.html.haml
@@ -4,7 +4,7 @@
- elsif can_remove_branch?(@merge_request.source_project, @merge_request.source_branch) && @merge_request.merged?
.remove_source_branch_widget
%p Changes merged into #{@merge_request.target_branch}. You can remove source branch now
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-small remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
%i.fa.fa-times
Remove Source Branch
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 dcf56541db8..62360158ff9 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,26 +1,24 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
.pull-right
- if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-small edit-milestone-link btn-grouped" do
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-sm edit-milestone-link btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-small btn-close"
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close"
%h4
= link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- if milestone.expired? and not milestone.closed?
%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/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index b1bc3ba0eba..c09815a212a 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1 +1,2 @@
+- page_title "Edit", @milestone.title, "Milestones"
= render "form"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index d3eab8d6d75..995eecd7830 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Milestones"
.pull-right
- if can? current_user, :admin_milestone, @project
= link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index b1bc3ba0eba..47149dfea41 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1 +1,2 @@
+- page_title "New Milestone"
= render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 110d8967342..bba2b8764ac 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @milestone.title, "Milestones"
%h4.page-title
.issue-box{ class: issue_box_class(@milestone) }
- if @milestone.closed?
@@ -60,11 +61,12 @@
Participants
%span.badge= @users.count
- .pull-right
- = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
- %i.fa.fa-plus
- New Issue
- = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped"
+ - if @project.issues_enabled
+ .pull-right
+ = link_to new_namespace_project_issue_path(@project.namespace, @project, issue: { milestone_id: @milestone.id }), class: "btn btn-grouped", title: "New Issue" do
+ %i.fa.fa-plus
+ New Issue
+ = link_to 'Browse Issues', namespace_project_issues_path(@milestone.project.namespace, @milestone.project, milestone_id: @milestone.id), class: "btn edit-milestone-link btn-grouped"
.tab-content
.tab-pane.active#tab-issues
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index c36bad1e94b..c67a7d256a8 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,3 +1,4 @@
+- page_title "Network", @ref
= render "head"
.project-network
.controls
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 00b912742b2..e56d8615132 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -1,3 +1,5 @@
+- page_title 'New Project'
+- header_title 'New Project'
.project-edit-container
.project-edit-errors
= render 'projects/errors'
@@ -21,71 +23,69 @@
= f.select :namespace_id, namespaces_options(params[:namespace_id] || :current_user), {}, {class: 'select2', tabindex: 2}
%hr
- .js-toggle-container
+
+ .project-import.js-toggle-container
.form-group
- .col-sm-2
+ %label.control-label Import project from
.col-sm-10
- = link_to "#", class: 'js-toggle-button' do
- %i.fa.fa-upload
- %span Import existing repository by URL
+ - if github_import_enabled?
+ = link_to status_import_github_path, class: 'btn' do
+ %i.fa.fa-github
+ GitHub
+ - else
+ = link_to '#', class: 'how_to_import_link light btn' do
+ %i.fa.fa-github
+ GitHub
+ = render 'github_import_modal'
+
+
+ - if bitbucket_import_enabled?
+ = link_to status_import_bitbucket_path, class: 'btn' do
+ %i.fa.fa-bitbucket
+ Bitbucket
+ - else
+ = link_to '#', class: 'how_to_import_link light btn' do
+ %i.fa.fa-bitbucket
+ Bitbucket
+ = render 'bitbucket_import_modal'
+
+ - unless request.host == 'gitlab.com'
+ - if gitlab_import_enabled?
+ = link_to status_import_gitlab_path, class: 'btn' do
+ %i.fa.fa-heart
+ GitLab.com
+ - else
+ = link_to '#', class: 'how_to_import_link light btn' do
+ %i.fa.fa-heart
+ GitLab.com
+ = render 'gitlab_import_modal'
+
+ = link_to new_import_gitorious_path, class: 'btn' do
+ %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
+
.js-toggle-content.hide
.form-group.import-url-data
= f.label :import_url, class: 'control-label' do
- %span Import existing git repo
+ %span Git repository URL
.col-sm-10
- = f.text_field :import_url, class: 'form-control', placeholder: 'https://github.com/randx/six.git'
- .alert.alert-info.prepend-top-10
- This URL must be publicly accessible or you can add a username and password like this: https://username:password@gitlab.com/company/project.git.
- %br
- The import will time out after 4 minutes. For big repositories, use a clone/push combination.
- For SVN repositories, check #{link_to "this migrating from SVN doc.", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}
-
- .project-import.form-group
- .col-sm-2
- .col-sm-10
- - if github_import_enabled?
- = link_to status_import_github_path do
- %i.fa.fa-github
- Import projects from GitHub
- - else
- = link_to '#', class: 'how_to_import_link light' do
- %i.fa.fa-github
- Import projects from GitHub
- = render 'github_import_modal'
-
- .project-import.form-group
- .col-sm-2
- .col-sm-10
- - if bitbucket_import_enabled?
- = link_to status_import_bitbucket_path do
- %i.fa.fa-bitbucket
- Import projects from Bitbucket
- - else
- = link_to '#', class: 'how_to_import_link light' do
- %i.fa.fa-bitbucket
- Import projects from Bitbucket
- = render 'bitbucket_import_modal'
-
- - unless request.host == 'gitlab.com'
- .project-import.form-group
- .col-sm-2
- .col-sm-10
- - if gitlab_import_enabled?
- = link_to status_import_gitlab_path do
- %i.fa.fa-heart
- Import projects from GitLab.com
- - else
- = link_to '#', class: 'how_to_import_link light' do
- %i.fa.fa-heart
- Import projects from GitLab.com
- = render 'gitlab_import_modal'
-
- .project-import.form-group
- .col-sm-2
- .col-sm-10
- = link_to new_import_gitorious_path do
- %i.icon-gitorious.icon-gitorious-small
- Import projects from Gitorious.org
+ = f.text_field :import_url, class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+ .well.prepend-top-20
+ %ul
+ %li
+ The repository must be accessible over HTTP(S). If it is not publicly accessible, you can add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ %li
+ The import will time out after 4 minutes. For big repositories, use a clone/push combination.
+ %li
+ To migrate an SVN repository, check out #{link_to "this document", "http://doc.gitlab.com/ce/workflow/migrating_from_svn.html"}.
%hr.prepend-botton-10
@@ -95,7 +95,7 @@
%span.light (optional)
.col-sm-10
= f.text_area :description, placeholder: "Awesome project", class: "form-control", rows: 3, maxlength: 250, tabindex: 3
- = render "visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true
+ = render 'shared/visibility_level', f: f, visibility_level: default_project_visibility, can_change_visibility_level: true, form_model: @project
.form-actions
= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4
@@ -104,7 +104,7 @@
.pull-right
.light
Need a group for several dependent projects?
- = link_to new_group_path, class: "btn btn-tiny" do
+ = link_to new_group_path, class: "btn btn-xs" do
Create a group
.save-project-loader.hide
@@ -115,9 +115,8 @@
%p Please wait a moment, this page will automatically refresh when ready.
:coffeescript
- $ ->
- $('.how_to_import_link').bind 'click', (e) ->
- e.preventDefault()
- import_modal = $(this).parent().find(".modal").show()
- $('.modal-header .close').bind 'click', ->
- $(".modal").hide()
+ $('.how_to_import_link').bind 'click', (e) ->
+ e.preventDefault()
+ import_modal = $(this).next(".modal").show()
+ $('.modal-header .close').bind 'click', ->
+ $(".modal").hide()
diff --git a/app/views/projects/notes/_discussion.html.haml b/app/views/projects/notes/_discussion.html.haml
index f4c6fad2fed..b8068835b3a 100644
--- a/app/views/projects/notes/_discussion.html.haml
+++ b/app/views/projects/notes/_discussion.html.haml
@@ -2,12 +2,12 @@
.timeline-entry
.timeline-entry-inner
.timeline-icon
- = image_tag avatar_icon(note.author_email), class: "avatar s40"
+ = link_to user_path(note.author) do
+ = 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/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index acb3991d294..a663950f031 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,15 +1,14 @@
.note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
= note_target_fields(note)
- = render layout: 'projects/md_preview', locals: { preview_class: "note-text" } do
- = render 'projects/zen', f: f, attr: :note,
- classes: 'note_text js-note-text'
+ = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do
+ = render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
.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 }.
+ .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 }.
.note-form-actions
.buttons
- = f.submit 'Save Comment', class: "btn btn-primary btn-save btn-grouped js-comment-button"
- = link_to 'Cancel', "#", class: "btn btn-cancel note-edit-cancel" \ No newline at end of file
+ = f.submit 'Save Comment', class: 'btn btn-primary btn-save btn-grouped js-comment-button'
+ = link_to 'Cancel', '#', class: 'btn btn-cancel note-edit-cancel'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 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 f3d00a6f06d..4d26b52df01 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -2,26 +2,28 @@
.timeline-entry-inner
.timeline-icon
- if note.system
- %span.fa.fa-circle
+ %span= icon('circle')
- else
- = image_tag avatar_icon(note.author_email), class: "avatar s40"
+ = link_to user_path(note.author) do
+ = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
.timeline-content
.note-header
.note-actions
= link_to "##{dom_id(note)}", name: dom_id(note) do
- %i.fa.fa-link
+ = icon('link')
Link here
&nbsp;
- - if can?(current_user, :admin_note, note) && note.editable?
- = link_to "#", title: "Edit comment", class: "js-note-edit" do
- %i.fa.fa-pencil-square-o
+ - if note_editable?(note)
+ = link_to '#', title: 'Edit comment', class: 'js-note-edit' do
+ = icon('pencil-square-o')
Edit
&nbsp;
- = link_to namespace_project_note_path(@project.namespace, @project, note), title: "Remove comment", method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: "danger js-note-delete" do
- %i.fa.fa-trash-o.cred
+ = link_to namespace_project_note_path(@project.namespace, @project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'danger js-note-delete' do
+ = icon('trash-o', class: 'cred')
Remove
- if note.system
- = image_tag avatar_icon(note.author_email), class: "avatar s16"
+ = link_to user_path(note.author) do
+ = 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
@@ -31,24 +33,24 @@
- if note.superceded?(@notes)
- if note.upvote?
%span.vote.upvote.label.label-gray.strikethrough
- %i.fa.fa-thumbs-up
+ = icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-gray.strikethrough
- %i.fa.fa-thumbs-down
+ = icon('thumbs-down')
\-1
- else
- if note.upvote?
%span.vote.upvote.label.label-success
- %i.fa.fa-thumbs-up
+ = icon('thumbs-up')
\+1
- if note.downvote?
%span.vote.downvote.label.label-danger
- %i.fa.fa-thumbs-down
+ = icon('thumbs-down')
\-1
- .note-body
+ .note-body{class: note_editable?(note) ? 'js-task-list-container' : ''}
.note-text
= preserve do
= markdown(note.note, {no_header_anchors: true})
@@ -60,10 +62,10 @@
= link_to note.attachment.url, target: '_blank' do
= image_tag note.attachment.url, class: 'note-image-attach'
.attachment
- = link_to note.attachment.url, target: "_blank" do
- %i.fa.fa-paperclip
+ = link_to note.attachment.url, target: '_blank' do
+ = icon('paperclip')
= note.attachment_identifier
= link_to delete_attachment_namespace_project_note_path(@project.namespace, @project, note),
- title: "Delete this attachment", method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: "danger js-note-attachment-delete" do
- %i.fa.fa-trash-o.cred
+ title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
+ = icon('trash-o', class: 'cred')
.clear
diff --git a/app/views/projects/notes/discussions/_diff.html.haml b/app/views/projects/notes/discussions/_diff.html.haml
index f717c77a898..711aa39101b 100644
--- a/app/views/projects/notes/discussions/_diff.html.haml
+++ b/app/views/projects/notes/discussions/_diff.html.haml
@@ -2,13 +2,13 @@
- if diff
.diff-file
.diff-header
- - if diff.deleted_file
- %span= diff.old_path
- - else
- %span= diff.new_path
- - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
- %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}"
- %br/
+ %span
+ - if diff.deleted_file
+ = diff.old_path
+ - else
+ = diff.new_path
+ - if diff.a_mode && diff.b_mode && diff.a_mode != diff.b_mode
+ %span.file-mode= "#{diff.a_mode} → #{diff.b_mode}"
.diff-content
%table
- note.truncated_diff_lines.each do |line|
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
new file mode 100644
index 00000000000..43e92437cf5
--- /dev/null
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -0,0 +1,16 @@
+.panel.panel-default
+ .panel-heading
+ %strong #{@group.name}
+ group members
+ %small
+ (#{members.count})
+ .panel-head-actions
+ = link_to group_group_members_path(@group), class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit group members
+ %ul.well-list
+ - members.each do |member|
+ = render 'groups/group_members/group_member', member: member, show_controls: false
+ - if members.count > 20
+ %li
+ and #{members.count - 20} more. For full list visit #{link_to 'group members page', group_group_members_path(@group)}
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
new file mode 100644
index 00000000000..d708b01a114
--- /dev/null
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -0,0 +1,18 @@
+= 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', 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'
+ .col-sm-10
+ = select_tag :access_level, options_for_select(ProjectMember.access_roles, @project_member.access_level), class: "project-access-select select2"
+ .help-block
+ Read more about role permissions
+ %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
+
+ .form-actions
+ = f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/_project_member.html.haml b/app/views/projects/project_members/_project_member.html.haml
new file mode 100644
index 00000000000..635e4d70941
--- /dev/null
+++ b/app/views/projects/project_members/_project_member.html.haml
@@ -0,0 +1,53 @@
+- user = member.user
+- return unless user || member.invite?
+
+%li{class: "#{dom_class(member)} js-toggle-container project_member_row access-#{member.human_access.downcase}", id: dom_id(member)}
+ %span.list-item-name
+ - if member.user
+ = image_tag avatar_icon(user.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
+ .pull-right
+ %strong= member.human_access
+ = button_tag class: "btn-xs btn js-toggle-button",
+ title: 'Edit access level', type: 'button' do
+ %i.fa.fa-pencil-square-o
+
+ &nbsp;
+ - if current_user == user
+ = 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, member), data: { confirm: remove_from_project_team_message(@project, member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from team' do
+ %i.fa.fa-minus.fa-inverse
+
+ .edit-member.hide.js-toggle-content
+ %br
+ = form_for member, as: :project_member, url: namespace_project_project_member_path(@project.namespace, @project, member), remote: true do |f|
+ .prepend-top-10
+ = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: 'form-control'
+ .prepend-top-10
+ = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
new file mode 100644
index 00000000000..615c425e59a
--- /dev/null
+++ b/app/views/projects/project_members/_team.html.haml
@@ -0,0 +1,11 @@
+- can_admin_project = can?(current_user, :admin_project, @project)
+
+.panel.panel-default.prepend-top-20
+ .panel-heading
+ %strong #{@project.name}
+ project members
+ %small
+ (#{members.count})
+ %ul.well-list
+ - members.each do |project_member|
+ = render 'project_member', member: project_member, current_user_can_admin_project: can_admin_project
diff --git a/app/views/projects/team_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 9e31d47117e..6914543f6da 100644
--- a/app/views/projects/team_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -1,14 +1,15 @@
+- page_title "Import members"
%h3.page-title
Import members from another project
%p.light
Only project members will be imported. Group members will be skipped.
%hr
-= form_tag apply_import_namespace_project_team_members_path(@project.namespace, @project), method: 'post', class: 'form-horizontal' do
+= form_tag apply_import_namespace_project_project_members_path(@project.namespace, @project), method: 'post', class: 'form-horizontal' do
.form-group
= label_tag :source_project_id, "Project", class: 'control-label'
.col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
- = link_to "Cancel", namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to "Cancel", namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
new file mode 100644
index 00000000000..6edb92acd4d
--- /dev/null
+++ b/app/views/projects/project_members/index.html.haml
@@ -0,0 +1,36 @@
+- page_title "Members"
+%h3.page-title
+ Users with access to this project
+
+%p.light
+ Read more about project permissions
+ %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
+
+%hr
+
+.clearfix.js-toggle-container
+ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input input-mn-300' }
+ = button_tag 'Search', class: 'btn'
+
+ - if can?(current_user, :admin_project_member, @project)
+ %span.pull-right
+ = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
+ Add members
+ %i.fa.fa-chevron-down
+ = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
+ Import members
+
+ .js-toggle-content.hide.new-group-member-holder
+ = render "new_project_member"
+
+= render "team", members: @project_members
+
+- if @group
+ = render "group_members", members: @group_members
+
+:coffeescript
+ $('form.member-search-form').on 'submit', (event) ->
+ event.preventDefault()
+ Turbolinks.visit @.action + '?' + $(@).serialize()
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
new file mode 100644
index 00000000000..811b1858821
--- /dev/null
+++ b/app/views/projects/project_members/update.js.haml
@@ -0,0 +1,3 @@
+- can_admin_project = can?(current_user, :admin_project, @project)
+:plain
+ $("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render("project_member", member: @project_member, current_user_can_admin_project: can_admin_project))}');
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
index 5406b80dc16..bb49f4de873 100644
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/_branches_list.html.haml
@@ -31,4 +31,4 @@
%td
.pull-right
- if can? current_user, :admin_project, @project
- = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-small"
+ = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, branch], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
diff --git a/app/views/projects/protected_branches/index.html.haml b/app/views/projects/protected_branches/index.html.haml
index cfe28084170..52b3a50c1e6 100644
--- a/app/views/projects/protected_branches/index.html.haml
+++ b/app/views/projects/protected_branches/index.html.haml
@@ -1,8 +1,9 @@
+- page_title "Protected branches"
%h3.page-title Protected branches
%p.light Keep stable branches secure and force developers to use Merge Requests
%hr
-.alert.alert-info
+.well.append-bottom-20
%p Protected branches are designed to
%ul
%li prevent pushes from everybody except #{link_to "masters", help_page_path("permissions", "permissions"), class: "vlink"}
@@ -23,12 +24,12 @@
.col-sm-10
= f.select(:name, @project.open_branches.map { |br| [br.name, br.name] } , {include_blank: "Select branch"}, {class: "select2"})
.form-group
- = f.label :developers_can_push, class: 'control-label' do
- Developers can push
- .col-sm-10
+ .col-sm-offset-2.col-sm-10
.checkbox
- = f.check_box :developers_can_push
- %span.descr Allow developers to push to this branch
+ = f.label :developers_can_push do
+ = f.check_box :developers_can_push
+ %strong Developers can push
+ .help-block Allow developers to push to this branch
.form-actions
= f.submit 'Protect', class: "btn-create btn"
= render 'branches_list'
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/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index eda59e6708b..e1823b51198 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -10,96 +10,12 @@
%hr
-= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |f|
- - if @service.errors.any?
- .alert.alert-danger
- %ul
- - @service.errors.full_messages.each do |msg|
- %li= msg
-
- - if @service.help.present?
- .alert.alert-info
- = preserve do
- = markdown @service.help
-
- .form-group
- = f.label :active, "Active", class: "control-label"
- .col-sm-10
- = f.check_box :active
-
- .form-group
- = f.label :url, "Trigger", class: 'control-label'
- - if @service.supported_events.length > 1
- .col-sm-10
- - if @service.supported_events.include?("push")
- %div
- = f.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This url will be triggered by a push to the repository
- - if @service.supported_events.include?("tag_push")
- %div
- = f.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This url will be triggered when a new tag is pushed to the repository
- - if @service.supported_events.include?("note")
- %div
- = f.check_box :note_events, class: 'pull-left'
- .prepend-left-20
- = f.label :note_events, class: 'list-label' do
- %strong Comments
- %p.light
- This url will be triggered when someone adds a comment
- - if @service.supported_events.include?("issue")
- %div
- = f.check_box :issues_events, class: 'pull-left'
- .prepend-left-20
- = f.label :issues_events, class: 'list-label' do
- %strong Issues events
- %p.light
- This url will be triggered when an issue is created
- - if @service.supported_events.include?("merge_request")
- %div
- = f.check_box :merge_requests_events, class: 'pull-left'
- .prepend-left-20
- = f.label :merge_requests_events, class: 'list-label' do
- %strong Merge Request events
- %p.light
- This url will be triggered when a merge request is created
-
- - @service.fields.each do |field|
- - name = field[:name]
- - title = field[:title] || name.humanize
- - value = @service.send(name) unless field[:type] == 'password'
- - type = field[:type]
- - placeholder = field[:placeholder]
- - choices = field[:choices]
- - default_choice = field[:default_choice]
- - help = field[:help]
-
- .form-group
- = f.label name, title, class: "control-label"
- .col-sm-10
- - if type == 'text'
- = f.text_field name, class: "form-control", placeholder: placeholder
- - elsif type == 'textarea'
- = f.text_area name, rows: 5, class: "form-control", placeholder: placeholder
- - elsif type == 'checkbox'
- = f.check_box name
- - elsif type == 'select'
- = f.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- - elsif type == 'password'
- = f.password_field name, class: 'form-control'
- - if help
- %span.help-block= help
+= form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
+ = render 'shared/service_settings', form: form
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = form.submit 'Save', class: 'btn btn-save'
&nbsp;
- - if @service.valid? && @service.activated? && @service.can_test?
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: 'btn'
+ - if @service.valid? && @service.activated?
+ - disabled = @service.can_test? ? '':'disabled'
+ = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service.to_param), class: "btn #{disabled}"
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index bcc5832792f..50ed78286d2 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1 +1,2 @@
+- page_title @service.title, "Services"
= render 'form'
diff --git a/app/views/projects/services/index.html.haml b/app/views/projects/services/index.html.haml
index 0d3ccb6bb83..1065def693b 100644
--- a/app/views/projects/services/index.html.haml
+++ b/app/views/projects/services/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Services"
%h3.page-title Project services
%p.light Project services allow you to integrate GitLab with other applications
diff --git a/app/views/projects/show.atom.builder b/app/views/projects/show.atom.builder
new file mode 100644
index 00000000000..bb713dcafa5
--- /dev/null
+++ b/app/views/projects/show.atom.builder
@@ -0,0 +1,12 @@
+xml.instruct!
+xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
+ xml.title "#{@project.name} activity"
+ xml.link href: namespace_project_url(@project.namespace, @project, format: :atom, private_token: current_user.private_token), rel: "self", type: "application/atom+xml"
+ xml.link href: namespace_project_url(@project.namespace, @project), rel: "alternate", type: "text/html"
+ xml.id namespace_project_url(@project.namespace, @project)
+ xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
+
+ @events.each do |event|
+ event_to_atom(xml, event)
+ end
+end
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 74b07395650..2259dea0865 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,89 +1,16 @@
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "#{@project.name} activity")
+
- if current_user && can?(current_user, :download_code, @project)
= render 'shared/no_ssh'
= render 'shared/no_password'
= render "home_panel"
+= render 'shared/show_aside'
-- readme = @repository.readme
-%ul.nav.nav-tabs
- %li.active
- = link_to '#tab-activity', 'data-toggle' => 'tab' do
- Activity
- - if readme
- %li
- = link_to '#tab-readme', 'data-toggle' => 'tab' do
- Readme
- .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)
- = link_to pluralize(number_with_delimiter(@repository.branch_names.count), 'branch'), namespace_project_branches_path(@project.namespace, @project)
- = link_to pluralize(number_with_delimiter(@repository.tag_names.count), 'tag'), namespace_project_tags_path(@project.namespace, @project)
- %span.light.prepend-left-20= repository_size
-
-.tab-content
- .tab-pane.active#tab-activity
- .row
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
- %section.col-md-9
- = render "events/event_last_push", event: @last_push
- = render 'shared/event_filter'
- .content_list
- = spinner
- %aside.col-md-3.project-side
- .clearfix
- - if @project.archived?
- .alert.alert-warning
- %h4
- %i.fa.fa-exclamation-triangle
- Archived project!
- %p Repository is read-only
-
- - if @project.forked_from_project
- .well
- %i.fa.fa-code-fork.project-fork-icon
- Forked from:
- %br
- = link_to @project.forked_from_project.name_with_namespace, namespace_project_path(@project.namespace, @project.forked_from_project)
-
- - 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
- Compare code
-
- - if @repository.version
- - version = @repository.version
- = link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, version.name)), class: 'btn btn-block' do
- Version:
- %span.count
- = @repository.blob_by_oid(version.id).data
-
- .prepend-top-10
- %p
- %span.light Created on
- #{@project.created_at.stamp('Aug 22, 2013')}
- %p
- %span.light Owned by
- - if @project.group
- #{link_to @project.group.name, @project.group} group
- - else
- #{link_to @project.owner_name, @project.owner}
-
- - @project.ci_services.each do |ci_service|
- - if ci_service.active? && ci_service.respond_to?(:builds_path)
- - 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"
- - else
- %span.light CI provided by
- = link_to ci_service.title, ci_service.builds_path, :'data-no-turbolink' => 'data-no-turbolink'
-
- - if 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
- %h4.readme-file-title
- %i.fa.fa-file
- = readme.name
- .wiki
- = render_readme(readme)
-
+.row
+ %section.col-md-8
+ = render 'section'
+ %aside.col-md-4.project-side
+ = render 'aside'
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 2d4d5d030ab..945f0084dff 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,4 +1,5 @@
+- page_title "Edit", @snippet.title, "Snippets"
%h3.page-title
Edit snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet)
+= render "shared/snippets/form", url: namespace_project_snippet_path(@project.namespace, @project, @snippet), visibility_level: @snippet.visibility_level
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index e2d8ec673a1..da9401bd8c1 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Snippets"
%h3.page-title
Snippets
- if can? current_user, :write_project_snippet, @project
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index bb659dba0cf..e38d95c45e7 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,4 +1,5 @@
+- page_title "New Snippets"
%h3.page-title
New snippet
%hr
-= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet)
+= render "shared/snippets/form", url: namespace_project_snippets_path(@project.namespace, @project, @snippet), visibility_level: default_snippet_visibility
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 345848fa6d1..5725d804df3 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @snippet.title, "Snippets"
%h3.page-title
= @snippet.title
@@ -23,15 +24,15 @@
.file-holder
.file-title
%i.fa.fa-file
- %span.file_name
+ %strong
= @snippet.file_name
- .options
+ .file-actions
.btn-group
- if can?(current_user, :modify_project_snippet, @snippet)
- = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", title: 'Edit Snippet'
- = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-small", target: "_blank"
+ = link_to "edit", edit_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", title: 'Edit Snippet'
+ = link_to "raw", raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
- if can?(current_user, :admin_project_snippet, @snippet)
- = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet'
+ = link_to "remove", namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
= render 'shared/snippets/blob'
%div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 8da07222cba..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-small'
+ = 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-small 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/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index f1bc2bc9a2b..d4652a47cba 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Tags"
= render "projects/commits/head"
%h3.page-title
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 655044438d5..172fafdeeff 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,3 +1,4 @@
+- page_title "New Tag"
- if @error
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
diff --git a/app/views/projects/team_members/_form.html.haml b/app/views/projects/team_members/_form.html.haml
deleted file mode 100644
index 166b6362a07..00000000000
--- a/app/views/projects/team_members/_form.html.haml
+++ /dev/null
@@ -1,29 +0,0 @@
-%h3.page-title
- New project member(s)
-
-= form_for @user_project_relation, as: :project_member, url: namespace_project_team_members_path(@project.namespace, @project), html: { class: "form-horizontal users-project-form" } do |f|
- -if @user_project_relation.errors.any?
- .alert.alert-danger
- %ul
- - @user_project_relation.errors.full_messages.each do |msg|
- %li= msg
-
- %p 1. Choose people you want in the project
- .form-group
- = f.label :user_ids, "People", class: 'control-label'
- .col-sm-10
- = users_select_tag(:user_ids, multiple: true)
-
- %p 2. Set access level for them
- .form-group
- = f.label :access_level, "Project Access", class: 'control-label'
- .col-sm-10
- = select_tag :access_level, options_for_select(Gitlab::Access.options, @user_project_relation.access_level), class: "project-access-select select2"
- .help-block
- Read more about role permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-
-
- .form-actions
- = f.submit 'Add users', class: "btn btn-create"
- = link_to "Cancel", namespace_project_team_index_path(@project.namespace, @project), class: "btn btn-cancel"
diff --git a/app/views/projects/team_members/_group_members.html.haml b/app/views/projects/team_members/_group_members.html.haml
deleted file mode 100644
index df3c914fdea..00000000000
--- a/app/views/projects/team_members/_group_members.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-- group_users_count = @group.group_members.count
-.panel.panel-default
- .panel-heading
- %strong #{@group.name}
- group members (#{group_users_count})
- .pull-right
- = link_to members_group_path(@group), class: 'btn btn-small' do
- %i.fa.fa-pencil-square-o
- %ul.well-list
- - @group.group_members.order('access_level DESC').limit(20).each do |member|
- = render 'groups/group_members/group_member', member: member, show_controls: false
- - if group_users_count > 20
- %li
- and #{group_users_count - 20} more. For full list visit #{link_to 'group members page', members_group_path(@group)}
diff --git a/app/views/projects/team_members/_team.html.haml b/app/views/projects/team_members/_team.html.haml
deleted file mode 100644
index 0e5b8176132..00000000000
--- a/app/views/projects/team_members/_team.html.haml
+++ /dev/null
@@ -1,9 +0,0 @@
-.team-table
- - can_admin_project = (can? current_user, :admin_project, @project)
- .panel.panel-default
- .panel-heading
- %strong #{@project.name}
- project members (#{members.count})
- %ul.well-list
- - members.each do |team_member|
- = render 'team_member', member: team_member, current_user_can_admin_project: can_admin_project
diff --git a/app/views/projects/team_members/_team_member.html.haml b/app/views/projects/team_members/_team_member.html.haml
deleted file mode 100644
index 61c50af31bf..00000000000
--- a/app/views/projects/team_members/_team_member.html.haml
+++ /dev/null
@@ -1,17 +0,0 @@
-- user = member.user
-%li{id: dom_id(user), class: "team_member_row access-#{member.human_access.downcase}"}
- .pull-right
- - if current_user_can_admin_project
- - unless @project.personal? && user == current_user
- .pull-left
- = form_for(member, as: :project_member, url: namespace_project_team_member_path(@project.namespace, @project, member.user)) do |f|
- = f.select :access_level, options_for_select(ProjectMember.access_roles, member.access_level), {}, class: "trigger-submit"
- &nbsp;
- = link_to namespace_project_team_member_path(@project.namespace, @project, user), data: { confirm: remove_from_project_team_message(@project, user)}, method: :delete, class: "btn-tiny btn btn-remove", title: 'Remove user from team' do
- %i.fa.fa-minus.fa-inverse
- = image_tag avatar_icon(user.email, 32), class: "avatar s32"
- %p
- %strong= user.name
- %span.cgray= user.username
-
-
diff --git a/app/views/projects/team_members/index.html.haml b/app/views/projects/team_members/index.html.haml
deleted file mode 100644
index fcc879a58df..00000000000
--- a/app/views/projects/team_members/index.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-%h3.page-title
- Users with access to this project
-
- - if can? current_user, :admin_team_member, @project
- %span.pull-right
- = link_to new_namespace_project_team_member_path(@project.namespace, @project), class: "btn btn-new btn-grouped", title: "New project member" do
- New project member
- = link_to import_namespace_project_team_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
- Import members
-
-%p.light
- Read more about project permissions
- %strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-= render "team", members: @project_members
-- if @group
- = render "group_members"
diff --git a/app/views/projects/team_members/new.html.haml b/app/views/projects/team_members/new.html.haml
deleted file mode 100644
index b1bc3ba0eba..00000000000
--- a/app/views/projects/team_members/new.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render "form"
diff --git a/app/views/projects/team_members/update.js.haml b/app/views/projects/team_members/update.js.haml
deleted file mode 100644
index c68fe9574a2..00000000000
--- a/app/views/projects/team_members/update.js.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- if @user_project_relation.valid?
- :plain
- $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#529214"}, 1000);;
-- else
- :plain
- $("##{dom_id(@user_project_relation)}").effect("highlight", {color: "#D12F19"}, 1000);;
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/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index fc4616da6ec..72916cad182 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,9 +1,14 @@
+- page_title @path.presence || "Files", @ref
+= content_for :meta_tags do
+ - if current_user
+ = auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
+
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path
- if can? current_user, :download_code, @project
.tree-download-holder
- = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-small pull-right hidden-xs hidden-sm', split_button: true
+ = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'btn-group-sm pull-right hidden-xs hidden-sm', split_button: true
#tree-holder.tree-holder.clearfix
= render "tree", tree: @tree
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 5567f1af22a..3f1dce1050c 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,3 +1,4 @@
+- page_title "Edit", @page.title, "Wiki"
= render 'nav'
.pull-right
= render 'main_links'
@@ -9,5 +10,5 @@
.pull-right
- if @page.persisted? && can?(current_user, :admin_wiki, @project)
- = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-small btn-remove" do
+ = link_to namespace_project_wiki_path(@project.namespace, @project, @page), data: { confirm: "Are you sure you want to delete this page?"}, method: :delete, class: "btn btn-sm btn-remove" do
Delete this page
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index 48058124f97..ead99412406 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,3 +1,4 @@
+- page_title "Wiki"
%h3.page-title Empty page
%hr
.error_message
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 365edb524f4..825f2a161c4 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,3 +1,4 @@
+- page_title "Git Access", "Wiki"
= render 'nav'
.row
.col-sm-6
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 91291f753f7..673ec2d20e5 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,3 +1,4 @@
+- page_title "History", @page.title, "Wiki"
= render 'nav'
%h3.page-title
%span.light History for
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index ee233d9086f..890ff1aed73 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,3 +1,4 @@
+- page_title "All Pages", "Wiki"
= render 'nav'
%h3.page-title
All Pages
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index a6263e93f67..83cd4c66672 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @page.title, "Wiki"
= render 'nav'
%h3.page-title
= @page.title
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
new file mode 100644
index 00000000000..154332cb9a9
--- /dev/null
+++ b/app/views/search/_category.html.haml
@@ -0,0 +1,77 @@
+%ul.nav.nav-pills.search-filter
+ - if @project
+ %li{class: ("active" if @scope == 'blobs')}
+ = link_to search_filter_path(scope: 'blobs') do
+ = icon('code fw')
+ %span
+ Code
+ %span.badge
+ = @search_results.blobs_count
+ %li{class: ("active" if @scope == 'issues')}
+ = link_to search_filter_path(scope: 'issues') do
+ = icon('exclamation-circle fw')
+ %span
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ %li{class: ("active" if @scope == 'merge_requests')}
+ = link_to search_filter_path(scope: 'merge_requests') do
+ = icon('tasks fw')
+ %span
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+ %li{class: ("active" if @scope == 'notes')}
+ = link_to search_filter_path(scope: 'notes') do
+ = icon('comments fw')
+ %span
+ Comments
+ %span.badge
+ = @search_results.notes_count
+ %li{class: ("active" if @scope == 'wiki_blobs')}
+ = link_to search_filter_path(scope: 'wiki_blobs') do
+ = icon('book fw')
+ %span
+ Wiki
+ %span.badge
+ = @search_results.wiki_blobs_count
+
+ - elsif @show_snippets
+ %li{class: ("active" if @scope == 'snippet_blobs')}
+ = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
+ = icon('code fw')
+ %span
+ Snippet Contents
+ %span.badge
+ = @search_results.snippet_blobs_count
+ %li{class: ("active" if @scope == 'snippet_titles')}
+ = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
+ = icon('book fw')
+ %span
+ Titles and Filenames
+ %span.badge
+ = @search_results.snippet_titles_count
+
+ - else
+ %li{class: ("active" if @scope == 'projects')}
+ = link_to search_filter_path(scope: 'projects') do
+ = icon('bookmark fw')
+ %span
+ Projects
+ %span.badge
+ = @search_results.projects_count
+ %li{class: ("active" if @scope == 'issues')}
+ = link_to search_filter_path(scope: 'issues') do
+ = icon('exclamation-circle fw')
+ %span
+ Issues
+ %span.badge
+ = @search_results.issues_count
+ %li{class: ("active" if @scope == 'merge_requests')}
+ = link_to search_filter_path(scope: 'merge_requests') do
+ = icon('tasks fw')
+ %span
+ Merge requests
+ %span.badge
+ = @search_results.merge_requests_count
+
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index c635c04fb8f..e2d0cab9e79 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -1,5 +1,5 @@
.dropdown.inline
- %button.dropdown-toggle.btn.btn-small{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle.btn.btn{type: 'button', 'data-toggle' => 'dropdown'}
%i.fa.fa-tags
%span.light Group:
- if @group.present?
@@ -17,7 +17,7 @@
= group.name
.dropdown.inline.prepend-left-10.project-filter
- %button.dropdown-toggle.btn.btn-small{type: 'button', 'data-toggle' => 'dropdown'}
+ %button.dropdown-toggle.btn.btn{type: 'button', 'data-toggle' => 'dropdown'}
%i.fa.fa-tags
%span.light Project:
- if @project.present?
diff --git a/app/views/search/_form.html.haml b/app/views/search/_form.html.haml
new file mode 100644
index 00000000000..47016daf1f0
--- /dev/null
+++ b/app/views/search/_form.html.haml
@@ -0,0 +1,12 @@
+= form_tag search_path, method: :get, class: 'form-inline' do |f|
+ = hidden_field_tag :project_id, params[:project_id]
+ = hidden_field_tag :group_id, params[:group_id]
+ = hidden_field_tag :snippets, params[:snippets]
+ = hidden_field_tag :scope, params[:scope]
+ .search-holder.clearfix
+ .form-group
+ = search_field_tag :search, params[:search], placeholder: "Search for projects, issues etc", class: "form-control search-text-input input-mn-300", id: "dashboard_search", autofocus: true
+ = button_tag 'Search', class: "btn btn-primary"
+ - unless params[:snippets].eql? 'true'
+ .pull-right
+ = render 'filter'
diff --git a/app/views/search/_global_filter.html.haml b/app/views/search/_global_filter.html.haml
deleted file mode 100644
index 442bd84f930..00000000000
--- a/app/views/search/_global_filter.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-%ul.nav.nav-pills.nav-stacked.search-filter
- %li{class: ("active" if @scope == 'projects')}
- = link_to search_filter_path(scope: 'projects') do
- Projects
- .pull-right
- = @search_results.projects_count
- %li{class: ("active" if @scope == 'issues')}
- = link_to search_filter_path(scope: 'issues') do
- Issues
- .pull-right
- = @search_results.issues_count
- %li{class: ("active" if @scope == 'merge_requests')}
- = link_to search_filter_path(scope: 'merge_requests') do
- Merge requests
- .pull-right
- = @search_results.merge_requests_count
diff --git a/app/views/search/_project_filter.html.haml b/app/views/search/_project_filter.html.haml
deleted file mode 100644
index ad933502a28..00000000000
--- a/app/views/search/_project_filter.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-%ul.nav.nav-pills.nav-stacked.search-filter
- %li{class: ("active" if @scope == 'blobs')}
- = link_to search_filter_path(scope: 'blobs') do
- %i.fa.fa-code
- Code
- .pull-right
- = @search_results.blobs_count
- %li{class: ("active" if @scope == 'issues')}
- = link_to search_filter_path(scope: 'issues') do
- %i.fa.fa-exclamation-circle
- Issues
- .pull-right
- = @search_results.issues_count
- %li{class: ("active" if @scope == 'merge_requests')}
- = link_to search_filter_path(scope: 'merge_requests') do
- %i.fa.fa-code-fork
- Merge requests
- .pull-right
- = @search_results.merge_requests_count
- %li{class: ("active" if @scope == 'notes')}
- = link_to search_filter_path(scope: 'notes') do
- %i.fa.fa-comments
- Comments
- .pull-right
- = @search_results.notes_count
- %li{class: ("active" if @scope == 'wiki_blobs')}
- = link_to search_filter_path(scope: 'wiki_blobs') do
- %i.fa.fa-book
- Wiki
- .pull-right
- = @search_results.wiki_blobs_count
-
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 796dd752a4c..741c780ad96 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -1,28 +1,21 @@
-%h4
- #{@search_results.total_count} results found
- - unless @show_snippets
- - if @project
- for #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
- - elsif @group
- for #{link_to @group.name, @group}
+- if @search_results.empty?
+ = render partial: "search/results/empty"
+- else
+ .light
+ Search results for
+ %code
+ = @search_term
+ - unless @show_snippets
+ - if @project
+ in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
+ - elsif @group
+ in group #{link_to @group.name, @group}
-%hr
-
-.row
- .col-sm-3
- - if @project
- = render "project_filter"
- - elsif @show_snippets
- = render 'snippet_filter'
- - else
- = render "global_filter"
- .col-sm-9
+ %br
+ .results.prepend-top-10
.search-results
- - if @search_results.empty?
- = render partial: "search/results/empty", locals: { message: "We couldn't find any matching results" }
- - else
- = render partial: "search/results/#{@scope.singularize}", collection: @objects
- = paginate @objects, theme: 'gitlab'
+ = render partial: "search/results/#{@scope.singularize}", collection: @objects
+ = paginate @objects, theme: 'gitlab'
:javascript
$(".search-results .term").highlight("#{escape_javascript(params[:search])}");
diff --git a/app/views/search/_snippet_filter.html.haml b/app/views/search/_snippet_filter.html.haml
deleted file mode 100644
index 95d23fa9f47..00000000000
--- a/app/views/search/_snippet_filter.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-%ul.nav.nav-pills.nav-stacked.search-filter
- %li{class: ("active" if @scope == 'snippet_blobs')}
- = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
- %i.fa.fa-code
- Snippet Contents
- .pull-right
- = @search_results.snippet_blobs_count
- %li{class: ("active" if @scope == 'snippet_titles')}
- = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
- %i.fa.fa-book
- Titles and Filenames
- .pull-right
- = @search_results.snippet_titles_count
diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml
index 01fb8cd9b8e..05a63016c09 100644
--- a/app/views/search/results/_empty.html.haml
+++ b/app/views/search/results/_empty.html.haml
@@ -1,4 +1,6 @@
.search_box
.search_glyph
- %span.fa.fa-search
- %h4 #{message}
+ %h4
+ = icon('search')
+ We couldn't find any results matching
+ %code #{@search_term}
diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml
index 6fc2cdf6362..8af393777f0 100644
--- a/app/views/search/results/_snippet_blob.html.haml
+++ b/app/views/search/results/_snippet_blob.html.haml
@@ -13,12 +13,6 @@
.file-title
%i.fa.fa-file
%strong= snippet_blob[:snippet_object].file_name
- %span.options
- .btn-group.tree-btn-group.pull-right
- - if snippet_blob[:snippet_object].author == current_user
- = link_to "Edit", edit_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", title: 'Edit Snippet'
- = link_to "Delete", snippet_path(snippet_blob[:snippet_object]), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-tiny", title: 'Delete Snippet'
- = link_to "Raw", raw_snippet_path(snippet_blob[:snippet_object]), class: "btn btn-tiny", target: "_blank"
- if gitlab_markdown?(snippet_blob[:snippet_object].file_name)
.file-content.wiki
- snippet_blob[:snippet_chunks].each do |snippet|
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 5b4816e4c40..60f9e9ac9de 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -1,22 +1,7 @@
-= form_tag search_path, method: :get, class: 'form-horizontal' do |f|
- .search-holder.clearfix
- .form-group
- = label_tag :search, class: 'control-label' do
- %span Looking for
- .col-sm-6
- = search_field_tag :search, params[:search], placeholder: "issue 143", class: "form-control search-text-input", id: "dashboard_search"
- .col-sm-4
- = button_tag 'Search', class: "btn btn-create"
- .form-group
- .col-sm-2
- - unless params[:snippets].eql? 'true'
- .col-sm-10
- = render 'filter', f: f
- = hidden_field_tag :project_id, params[:project_id]
- = hidden_field_tag :group_id, params[:group_id]
- = hidden_field_tag :snippets, params[:snippets]
- = hidden_field_tag :scope, params[:scope]
-
- .results.prepend-top-10
- - if params[:search].present?
- = render 'search/results'
+- page_title @search_term
+= render 'search/form'
+%hr
+- if @search_term
+ = render 'search/category'
+ %hr
+ = render 'search/results'
diff --git a/app/views/shared/_choose_group_avatar_button.html.haml b/app/views/shared/_choose_group_avatar_button.html.haml
index 299c0bd42a2..000532b1c9a 100644
--- a/app/views/shared/_choose_group_avatar_button.html.haml
+++ b/app/views/shared/_choose_group_avatar_button.html.haml
@@ -1,4 +1,4 @@
-%a.choose-btn.btn.btn-small.js-choose-group-avatar-button
+%a.choose-btn.btn.btn-sm.js-choose-group-avatar-button
%i.fa.fa-paperclip
%span Choose File ...
&nbsp;
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index a1121750ca3..60bb76e898a 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -1,21 +1,23 @@
- project = project || @project
.git-clone-holder.input-group
- .input-group-btn
- %button{ |
- class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
- :"data-clone" => project.ssh_url_to_repo, |
- :"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH",
- :"data-html" => "true",
- :"data-container" => "body"}
- SSH
- %button{ |
- class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
- :"data-clone" => project.http_url_to_repo, |
- :"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}",
- :"data-html" => "true",
- :"data-container" => "body"}
- = gitlab_config.protocol.upcase
- = text_field_tag :project_clone, default_url_to_repo(project), class: "one_click_select form-control", readonly: true
+ .input-group-addon.git-protocols
+ .input-group-btn
+ %button{ |
+ class: "btn btn-sm #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
+ :"data-clone" => project.ssh_url_to_repo, |
+ :"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH",
+ :"data-html" => "true",
+ :"data-container" => "body"}
+ SSH
+ .input-group-btn
+ %button{ |
+ class: "btn btn-sm #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
+ :"data-clone" => project.http_url_to_repo, |
+ :"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}",
+ :"data-html" => "true",
+ :"data-container" => "body"}
+ = gitlab_config.protocol.upcase
+ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control input-sm", 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/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index d07a9e2b924..334db60690d 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -3,17 +3,3 @@
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
-
- - if current_user
- - if current_controller?(:dashboard)
- %li.pull-right
- = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do
- %i.fa.fa-rss
- News Feed
-
- - if current_controller?(:groups)
- %li.pull-right
- = link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do
- %i.fa.fa-rss
- News Feed
-%hr
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
new file mode 100644
index 00000000000..30d37dceb30
--- /dev/null
+++ b/app/views/shared/_field.html.haml
@@ -0,0 +1,24 @@
+- name = field[:name]
+- title = field[:title] || name.humanize
+- value = service_field_value(field[:type], @service.send(name))
+- type = field[:type]
+- placeholder = field[:placeholder]
+- choices = field[:choices]
+- default_choice = field[:default_choice]
+- help = field[:help]
+
+.form-group
+ = form.label name, title, class: "control-label"
+ .col-sm-10
+ - if type == 'text'
+ = form.text_field name, class: "form-control", placeholder: placeholder
+ - elsif type == 'textarea'
+ = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder
+ - elsif type == 'checkbox'
+ = form.check_box name
+ - elsif type == 'select'
+ = form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
+ - elsif type == 'password'
+ = form.password_field name, placeholder: value, class: 'form-control'
+ - if help
+ %span.help-block= help
diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml
index b34dd53e3b5..c0a9923348e 100644
--- a/app/views/shared/_group_form.html.haml
+++ b/app/views/shared/_group_form.html.haml
@@ -15,7 +15,7 @@
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false
- if @group.persisted?
- .alert.alert-danger
+ .alert.alert-warning.prepend-top-10
%ul
%li Changing group path can have unintended side effects.
%li Renaming group path will rename directory for all related projects
diff --git a/app/views/shared/_issuable_filter.html.haml b/app/views/shared/_issuable_filter.html.haml
index 5412b9ef0f4..fa8b4eae314 100644
--- a/app/views/shared/_issuable_filter.html.haml
+++ b/app/views/shared/_issuable_filter.html.haml
@@ -4,116 +4,55 @@
%li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened') do
%i.fa.fa-exclamation-circle
- Open
+ #{state_filters_text_for(:opened, @project)}
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed') do
%i.fa.fa-check-circle
- Closed
+ #{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all') do
%i.fa.fa-compass
- All
+ #{state_filters_text_for(:all, @project)}
- %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_title, :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_title', 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/_issuable_search_form.html.haml b/app/views/shared/_issuable_search_form.html.haml
new file mode 100644
index 00000000000..639d203dcd6
--- /dev/null
+++ b/app/views/shared/_issuable_search_form.html.haml
@@ -0,0 +1,9 @@
+= form_tag(path, method: :get, id: "issue_search_form", class: 'pull-left issue-search-form') do
+ .append-right-10.hidden-xs.hidden-sm
+ = search_field_tag :issue_search, params[:issue_search], { placeholder: 'Filter by title or description', class: 'form-control issue_search search-text-input input-mn-300' }
+ = hidden_field_tag :state, params['state']
+ = hidden_field_tag :scope, params['scope']
+ = hidden_field_tag :assignee_id, params['assignee_id']
+ = hidden_field_tag :author_id, params['author_id']
+ = hidden_field_tag :milestone_id, params['milestone_id']
+ = hidden_field_tag :label_id, params['label_id']
diff --git a/app/views/shared/_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/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
new file mode 100644
index 00000000000..16a98a7233c
--- /dev/null
+++ b/app/views/shared/_service_settings.html.haml
@@ -0,0 +1,75 @@
+- if @service.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ %ul
+ - @service.errors.full_messages.each do |msg|
+ %li= msg
+
+- if @service.help.present?
+ .well
+ = preserve do
+ = markdown @service.help
+
+.form-group
+ = form.label :active, "Active", class: "control-label"
+ .col-sm-10
+ = form.check_box :active
+
+- if @service.supported_events.length > 1
+ .form-group
+ = form.label :url, "Trigger", class: 'control-label'
+ .col-sm-10
+ - if @service.supported_events.include?("push")
+ %div
+ = form.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This url will be triggered by a push to the repository
+ - if @service.supported_events.include?("tag_push")
+ %div
+ = form.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This url will be triggered when a new tag is pushed to the repository
+ - if @service.supported_events.include?("note")
+ %div
+ = form.check_box :note_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :note_events, class: 'list-label' do
+ %strong Comments
+ %p.light
+ This url will be triggered when someone adds a comment
+ - if @service.supported_events.include?("issue")
+ %div
+ = form.check_box :issues_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :issues_events, class: 'list-label' do
+ %strong Issues events
+ %p.light
+ This url will be triggered when an issue is created
+ - if @service.supported_events.include?("merge_request")
+ %div
+ = form.check_box :merge_requests_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :merge_requests_events, class: 'list-label' do
+ %strong Merge Request events
+ %p.light
+ This url will be triggered when a merge request is created
+
+- @service.fields.each do |field|
+ - type = field[:type]
+
+ - if type == 'fieldset'
+ - fields = field[:fields]
+ - legend = field[:legend]
+
+ %fieldset
+ %legend= legend
+ - fields.each do |subfield|
+ = render 'shared/field', form: form, field: subfield
+ - else
+ = render 'shared/field', form: form, field: field
diff --git a/app/views/shared/_show_aside.html.haml b/app/views/shared/_show_aside.html.haml
new file mode 100644
index 00000000000..3ac9b11b4fa
--- /dev/null
+++ b/app/views/shared/_show_aside.html.haml
@@ -0,0 +1,2 @@
+= link_to '#aside', class: 'show-aside' do
+ %i.fa.fa-angle-left
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
new file mode 100644
index 00000000000..1c6ec198d3d
--- /dev/null
+++ b/app/views/shared/_visibility_level.html.haml
@@ -0,0 +1,14 @@
+.form-group.project-visibility-level-holder
+ = f.label :visibility_level, class: 'control-label' do
+ Visibility Level
+ = link_to "(?)", help_page_path("public_access", "public_access")
+ .col-sm-10
+ - if can_change_visibility_level
+ = render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
+ - else
+ .col-sm-10
+ %span.info
+ = visibility_level_icon(visibility_level)
+ %strong
+ = visibility_level_label(visibility_level)
+ .light= visibility_level_description(visibility_level, form_model)
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
new file mode 100644
index 00000000000..b07c4d20f12
--- /dev/null
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -0,0 +1,14 @@
+- Gitlab::VisibilityLevel.values.each do |level|
+ .radio
+ - restricted = restricted_visibility_levels.include?(level)
+ = label model_method, level do
+ = form.radio_button model_method, level, checked: (selected_level == level), disabled: restricted
+ = visibility_level_icon(level)
+ .option-title
+ = visibility_level_label(level)
+ .option-descr
+ = visibility_level_description(level, form_model)
+- unless restricted_visibility_levels.empty?
+ .col-sm-10
+ %span.info
+ Some visibility level settings have been restricted by the administrator.
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 4e0663ea208..6783587bda9 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -10,7 +10,7 @@
= f.label :title, class: 'control-label'
.col-sm-10= f.text_field :title, placeholder: "Example Snippet", class: 'form-control', required: true
- = render "shared/snippets/visibility_level", f: f, visibility_level: gitlab_config.default_projects_features.visibility_level, can_change_visibility_level: true
+ = render 'shared/visibility_level', f: f, visibility_level: visibility_level, can_change_visibility_level: true, form_model: @snippet
.form-group
.file-editor
diff --git a/app/views/shared/snippets/_visibility_level.html.haml b/app/views/shared/snippets/_visibility_level.html.haml
deleted file mode 100644
index 9acff18e450..00000000000
--- a/app/views/shared/snippets/_visibility_level.html.haml
+++ /dev/null
@@ -1,27 +0,0 @@
-.form-group.project-visibility-level-holder
- = f.label :visibility_level, class: 'control-label' do
- Visibility Level
- = link_to "(?)", help_page_path("public_access", "public_access")
- .col-sm-10
- - if can_change_visibility_level
- - Gitlab::VisibilityLevel.values.each do |level|
- .radio
- - restricted = restricted_visibility_levels.include?(level)
- = f.radio_button :visibility_level, level, disabled: restricted
- = label "#{dom_class(@snippet)}_visibility_level", level do
- = visibility_level_icon(level)
- .option-title
- = visibility_level_label(level)
- .option-descr
- = snippet_visibility_level_description(level)
- - unless restricted_visibility_levels.empty?
- .col-sm-10
- %span.info
- Some visibility level settings have been restricted by the administrator.
- - else
- .col-sm-10
- %span.info
- = visibility_level_icon(visibility_level)
- %strong
- = visibility_level_label(visibility_level)
- .light= visibility_level_description(visibility_level)
diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml
index b2b7ea4df0e..0718f743828 100644
--- a/app/views/snippets/current_user_index.html.haml
+++ b/app/views/snippets/current_user_index.html.haml
@@ -1,39 +1,35 @@
+- page_title "Your Snippets"
%h3.page-title
- My Snippets
+ Your Snippets
.pull-right
= link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do
Add new snippet
- = link_to snippets_path, class: "btn btn-grouped" do
- Discover snippets
%p.light
Share code pastes with others out of git repository
-%hr
-.row
- .col-md-3
- %ul.nav.nav-pills.nav-stacked
- = nav_tab :scope, nil do
- = link_to user_snippets_path(@user) do
- All
- %span.pull-right
- = @user.snippets.count
- = nav_tab :scope, 'are_private' do
- = link_to user_snippets_path(@user, scope: 'are_private') do
- Private
- %span.pull-right
- = @user.snippets.are_private.count
- = nav_tab :scope, 'are_internal' do
- = link_to user_snippets_path(@user, scope: 'are_internal') do
- Internal
- %span.pull-right
- = @user.snippets.are_internal.count
- = nav_tab :scope, 'are_public' do
- = link_to user_snippets_path(@user, scope: 'are_public') do
- Public
- %span.pull-right
- = @user.snippets.are_public.count
+%ul.nav.nav-tabs
+ = nav_tab :scope, nil do
+ = link_to user_snippets_path(@user) do
+ All
+ %span.badge
+ = @user.snippets.count
+ = nav_tab :scope, 'are_private' do
+ = link_to user_snippets_path(@user, scope: 'are_private') do
+ Private
+ %span.badge
+ = @user.snippets.are_private.count
+ = nav_tab :scope, 'are_internal' do
+ = link_to user_snippets_path(@user, scope: 'are_internal') do
+ Internal
+ %span.badge
+ = @user.snippets.are_internal.count
+ = nav_tab :scope, 'are_public' do
+ = link_to user_snippets_path(@user, scope: 'are_public') do
+ Public
+ %span.badge
+ = @user.snippets.are_public.count
- .col-md-9.my-snippets
- = render 'snippets'
+.my-snippets
+ = render 'snippets'
diff --git a/app/views/snippets/edit.html.haml b/app/views/snippets/edit.html.haml
index 7042d07d5e8..1a380035661 100644
--- a/app/views/snippets/edit.html.haml
+++ b/app/views/snippets/edit.html.haml
@@ -1,4 +1,5 @@
+- page_title "Edit", @snippet.title, "Snippets"
%h3.page-title
Edit snippet
%hr
-= render "shared/snippets/form", url: snippet_path(@snippet)
+= render 'shared/snippets/form', url: snippet_path(@snippet), visibility_level: @snippet.visibility_level
diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml
index 0d71c41e2e7..e9bb6a908d3 100644
--- a/app/views/snippets/index.html.haml
+++ b/app/views/snippets/index.html.haml
@@ -1,13 +1,13 @@
+- page_title "Public Snippets"
%h3.page-title
Public snippets
.pull-right
-
- if current_user
= link_to new_snippet_path, class: "btn btn-new btn-grouped", title: "New Snippet" do
Add new snippet
= link_to user_snippets_path(current_user), class: "btn btn-grouped" do
- My snippets
+ Your snippets
%p.light
Public snippets created by you and other users are listed here
diff --git a/app/views/snippets/new.html.haml b/app/views/snippets/new.html.haml
index 694d7058317..a74d5e792ad 100644
--- a/app/views/snippets/new.html.haml
+++ b/app/views/snippets/new.html.haml
@@ -1,4 +1,5 @@
+- page_title "New Snippet"
%h3.page-title
New snippet
%hr
-= render "shared/snippets/form", url: snippets_path(@snippet)
+= render "shared/snippets/form", url: snippets_path(@snippet), visibility_level: default_snippet_visibility
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index f5bc543de10..70a95abde6f 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,3 +1,4 @@
+- page_title @snippet.title, "Snippets"
%h3.page-title
= @snippet.title
@@ -17,13 +18,13 @@
%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
- if @snippet.author == current_user
= link_to user_snippets_path(current_user) do
- &larr; my snippets
+ &larr; your snippets
- else
= link_to snippets_path do
&larr; discover snippets
@@ -31,13 +32,13 @@
.file-holder
.file-title
%i.fa.fa-file
- %span.file_name
+ %strong
= @snippet.file_name
- .options
+ .file-actions
.btn-group
- if can?(current_user, :modify_personal_snippet, @snippet)
- = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-small", title: 'Edit Snippet'
- = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-small", target: "_blank"
+ = link_to "edit", edit_snippet_path(@snippet), class: "btn btn-sm", title: 'Edit Snippet'
+ = link_to "raw", raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
- if can?(current_user, :admin_personal_snippet, @snippet)
- = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-small btn-remove", title: 'Delete Snippet'
+ = link_to "remove", snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-sm btn-remove", title: 'Delete Snippet'
= render 'shared/snippets/blob'
diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml
index 67f3a68aa22..23700eb39da 100644
--- a/app/views/snippets/user_index.html.haml
+++ b/app/views/snippets/user_index.html.haml
@@ -1,3 +1,4 @@
+- page_title "Snippets", @user.name
%h3.page-title
= image_tag avatar_icon(@user.email), class: "avatar s24"
= @user.name
@@ -5,7 +6,7 @@
\/
Snippets
- if current_user
- = link_to new_snippet_path, class: "btn btn-small add_new pull-right", title: "New Snippet" do
+ = link_to new_snippet_path, class: "btn btn-sm add_new pull-right", title: "New Snippet" do
Add new snippet
%hr
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index cb84570a6d5..f360fbb3d5d 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -1,4 +1,4 @@
.clearfix
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
- = image_tag group_icon(group.path), class: 'avatar group-avatar s40'
+ = image_tag group_icon(group), class: 'avatar group-avatar s40'
diff --git a/app/views/users/_profile.html.haml b/app/views/users/_profile.html.haml
index 3b44959baad..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:
@@ -12,7 +16,7 @@
- unless user.linkedin.blank?
%li
%span.light LinkedIn:
- %strong= user.linkedin
+ %strong= link_to user.linkedin, "http://www.linkedin.com/in/#{user.linkedin}"
- unless user.twitter.blank?
%li
%span.light Twitter:
@@ -21,7 +25,7 @@
%li
%span.light Website:
%strong= link_to user.short_website_url, user.full_website_url
- - unless user.bio.blank?
+ - unless user.location.blank?
%li
- %span.light Bio:
- %span= user.bio
+ %span.light Location:
+ %strong= user.location
diff --git a/app/views/users/_projects.html.haml b/app/views/users/_projects.html.haml
index 6c7779be30e..297fa537394 100644
--- a/app/views/users/_projects.html.haml
+++ b/app/views/users/_projects.html.haml
@@ -1,13 +1,13 @@
-- if @contributed_projects.present?
- .panel.panel-default
+- 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/calendar.html.haml b/app/views/users/calendar.html.haml
index 1d1c974da24..922b0c6cebf 100644
--- a/app/views/users/calendar.html.haml
+++ b/app/views/users/calendar.html.haml
@@ -1,8 +1,12 @@
-%h4 Commits calendar
+%h4
+ Contributions calendar
+ .pull-right
+ %small Issues, merge requests and push events
#cal-heatmap.calendar
:javascript
- new calendar(
+ new Calendar(
#{@timestamps.to_json},
#{@starting_year},
- #{@starting_month}
+ #{@starting_month},
+ '#{user_calendar_activities_path}'
);
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
new file mode 100644
index 00000000000..027a93a75fc
--- /dev/null
+++ b/app/views/users/calendar_activities.html.haml
@@ -0,0 +1,23 @@
+%h4.prepend-top-20
+ %span.light Contributions for
+ %strong #{@calendar_date.to_s(:short)}
+
+%ul.bordered-list
+ - @events.sort_by(&:created_at).each do |event|
+ %li
+ %span.light
+ %i.fa.fa-clock-o
+ = event.created_at.to_s(:time)
+ - if event.push?
+ #{event.action_name} #{event.ref_type} #{event.ref_name}
+ - else
+ = event_action_name(event)
+ - if event.target
+ %strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
+
+ at
+ %strong
+ - if event.project
+ = link_to_project event.project
+ - else
+ = event.project_name
diff --git a/app/views/users/show.atom.builder b/app/views/users/show.atom.builder
index 8fe30b23635..50232dc7186 100644
--- a/app/views/users/show.atom.builder
+++ b/app/views/users/show.atom.builder
@@ -1,9 +1,9 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
- xml.title "Activity feed for #{@user.name}"
+ xml.title "#{@user.name} activity"
xml.link href: user_url(@user, :atom), rel: "self", type: "application/atom+xml"
xml.link href: user_url(@user), rel: "alternate", type: "text/html"
- xml.id projects_url
+ xml.id user_url(@user)
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index abd6b229782..6ed45fedfa2 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -1,30 +1,41 @@
+- page_title @user.name
+- header_title @user.name, user_path(@user)
+
+= content_for :meta_tags do
+ = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
+
+= render 'shared/show_aside'
+
.row
- = link_to '#aside', class: 'show-aside' do
- %i.fa.fa-angle-left
%section.col-md-8
- %h3.page-title
+ .header-with-avatar
= image_tag avatar_icon(@user.email, 90), class: "avatar avatar-tile s90", alt: ''
- = @user.name
- - if @user == current_user
- .pull-right
- = link_to profile_path, class: 'btn' do
- %i.fa.fa-pencil-square-o
- Edit Profile settings
- %br
- %span.user-show-username #{@user.username}
- %br
- %small member since #{@user.created_at.stamp("Nov 12, 2031")}
+ %h3
+ = @user.name
+ - if @user == current_user
+ .pull-right
+ = link_to profile_path, class: 'btn btn-sm' do
+ %i.fa.fa-pencil-square-o
+ Edit Profile settings
+ .username
+ @#{@user.username}
+ .description
+ - if @user.bio.present?
+ = @user.bio
+
.clearfix
- if @groups.any?
- %h4 Groups
- = render 'groups', groups: @groups
- %hr
+ .prepend-top-20
+ %h4 Groups
+ = render 'groups', groups: @groups
+ %hr
.hidden-xs
.user-calendar
%h4.center.light
%i.fa.fa-spinner.fa-spin
+ .user-calendar-activities
%hr
%h4
User Activity
@@ -35,11 +46,11 @@
%strong
%i.fa.fa-rss
- = render @events
+ .content_list
+ = 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}")
+ $(".user-calendar").load("#{user_calendar_path}")
diff --git a/app/views/votes/_votes_block.html.haml b/app/views/votes/_votes_block.html.haml
index 788d9065a7b..36ea6742064 100644
--- a/app/views/votes/_votes_block.html.haml
+++ b/app/views/votes/_votes_block.html.haml
@@ -1,6 +1,10 @@
.votes.votes-block
- .progress
- .progress-bar.progress-bar-success{style: "width: #{votable.upvotes_in_percent}%;"}
- .progress-bar.progress-bar-danger{style: "width: #{votable.downvotes_in_percent}%;"}
- .upvotes= "#{votable.upvotes} up"
- .downvotes= "#{votable.downvotes} down"
+ .btn-group
+ - unless votable.upvotes.zero?
+ .btn.btn-sm.disabled.cgreen
+ %i.fa.fa-thumbs-up
+ = votable.upvotes
+ - unless votable.downvotes.zero?
+ .btn.btn-sm.disabled.cred
+ %i.fa.fa-thumbs-down
+ = votable.downvotes
diff --git a/app/views/votes/_votes_inline.html.haml b/app/views/votes/_votes_inline.html.haml
index ee805474830..2cb3ae04e1a 100644
--- a/app/views/votes/_votes_inline.html.haml
+++ b/app/views/votes/_votes_inline.html.haml
@@ -1,9 +1,9 @@
.votes.votes-inline
- unless votable.upvotes.zero?
- .upvotes
+ %span.upvotes.cgreen
+ #{votable.upvotes}
- unless votable.downvotes.zero?
\/
- unless votable.downvotes.zero?
- .downvotes
+ %span.downvotes.cred
\- #{votable.downvotes}
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index 2e783814824..1d21addece6 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -1,40 +1,57 @@
class EmailsOnPushWorker
include Sidekiq::Worker
- def perform(project_id, recipients, push_data, send_from_committer_email = false, disable_diffs = false)
+ def perform(project_id, recipients, push_data, options = {})
+ options.symbolize_keys!
+ options.reverse_merge!(
+ send_from_committer_email: false,
+ disable_diffs: false
+ )
+ send_from_committer_email = options[:send_from_committer_email]
+ disable_diffs = options[:disable_diffs]
+
project = Project.find(project_id)
before_sha = push_data["before"]
after_sha = push_data["after"]
- branch = push_data["ref"]
+ ref = push_data["ref"]
author_id = push_data["user_id"]
- if before_sha =~ /^000000/ || after_sha =~ /^000000/
- # skip if new branch was pushed or branch was removed
- return true
- end
+ action =
+ if Gitlab::Git.blank_ref?(before_sha)
+ :create
+ elsif Gitlab::Git.blank_ref?(after_sha)
+ :delete
+ else
+ :push
+ end
- compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
+ compare = nil
+ reverse_compare = false
+ if action == :push
+ compare = Gitlab::Git::Compare.new(project.repository.raw_repository, before_sha, after_sha)
- return false if compare.same
+ return false if compare.same
- if compare.commits.empty?
- compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
+ if compare.commits.empty?
+ compare = Gitlab::Git::Compare.new(project.repository.raw_repository, after_sha, before_sha)
- reverse_compare = true
+ reverse_compare = true
- return false if compare.commits.empty?
+ return false if compare.commits.empty?
+ end
end
recipients.split(" ").each do |recipient|
Notify.repository_push_email(
project_id,
recipient,
- author_id,
- branch,
- compare,
- reverse_compare,
- send_from_committer_email,
- disable_diffs
+ author_id: author_id,
+ ref: ref,
+ action: action,
+ compare: compare,
+ reverse_compare: reverse_compare,
+ send_from_committer_email: send_from_committer_email,
+ disable_diffs: disable_diffs
).deliver
end
ensure
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/irker_worker.rb b/app/workers/irker_worker.rb
index 613bae351d8..84a54656df2 100644
--- a/app/workers/irker_worker.rb
+++ b/app/workers/irker_worker.rb
@@ -57,9 +57,9 @@ class IrkerWorker
end
def send_branch_updates(push_data, project, repo_name, committer, branch)
- if push_data['before'] =~ /^000000/
+ if Gitlab::Git.blank_ref?(push_data['before'])
send_new_branch project, repo_name, committer, branch
- elsif push_data['after'] =~ /^000000/
+ elsif Gitlab::Git.blank_ref?(push_data['after'])
send_del_branch repo_name, committer, branch
end
end
@@ -83,7 +83,7 @@ class IrkerWorker
return if push_data['total_commits_count'] == 0
# Next message is for number of commit pushed, if any
- if push_data['before'] =~ /^000000/
+ if Gitlab::Git.blank_ref?(push_data['before'])
# Tweak on push_data["before"] in order to have a nice compare URL
push_data['before'] = before_on_new_branch push_data, project
end
@@ -137,8 +137,7 @@ class IrkerWorker
end
def commit_from_id(project, id)
- commit = Gitlab::Git::Commit.find(project.repository, id)
- Commit.new(commit)
+ project.commit(id)
end
def files_count(commit)
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 1406cba2db3..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)
@@ -21,7 +21,9 @@ class PostReceive
return false
end
- changes = changes.lines if changes.kind_of?(String)
+ changes = Base64.decode64(changes) unless changes.include?(" ")
+ changes = utf8_encode_changes(changes)
+ changes = changes.lines
changes.each do |change|
oldrev, newrev, ref = change.strip.split(' ')
@@ -33,7 +35,7 @@ class PostReceive
return false
end
- if tag?(ref)
+ if Gitlab::Git.tag_ref?(ref)
GitTagPushService.new.execute(project, @user, oldrev, newrev, ref)
else
GitPushService.new.execute(project, @user, oldrev, newrev, ref)
@@ -41,13 +43,20 @@ class PostReceive
end
end
- def log(message)
- Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
- end
+ def utf8_encode_changes(changes)
+ changes = changes.dup
+
+ changes.force_encoding("UTF-8")
+ return changes if changes.valid_encoding?
- private
+ # Convert non-UTF-8 branch/tag names to UTF-8 so they can be dumped as JSON.
+ detection = CharlockHolmes::EncodingDetector.detect(changes)
+ return changes unless detection && detection[:encoding]
- def tag?(ref)
- !!(/refs\/tags\/(.*)/.match(ref))
+ CharlockHolmes::Converter.convert(changes, detection[:encoding], 'UTF-8')
+ end
+
+ def log(message)
+ Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
end
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/bin/guard b/bin/guard
new file mode 100755
index 00000000000..0c1a532bd01
--- /dev/null
+++ b/bin/guard
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'guard' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+ Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('guard', 'guard')
diff --git a/config/application.rb b/config/application.rb
index bd4578848c5..fa399533e52 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -50,6 +50,8 @@ module Gitlab
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
+ config.action_view.sanitized_allowed_protocols = %w(smb)
+
# Relative url support
# Uncomment and customize the last line to run in a non-root path
# WARNING: We recommend creating a FQDN to host GitLab in a root path instead of this.
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 6dff07cf9df..bd2081688d1 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -2,11 +2,19 @@
# GitLab application config file #
# # # # # # # # # # # # # # # # # #
#
+########################### NOTE #####################################
+# This file should not receive new settings. All configuration options #
+# are being moved to ApplicationSetting model! #
+########################################################################
+#
# How to use:
# 1. Copy file as gitlab.yml
# 2. Update gitlab -> host with your fully qualified domain name
# 3. Update gitlab -> email_from
# 4. If you installed Git from source, change git -> bin_path to /usr/local/bin/git
+# IMPORTANT: If Git was installed in a different location use that instead.
+# You can check with `which git`. If a wrong path of Git is specified, it will
+# result in various issues such as failures of GitLab CI builds.
# 5. Review this configuration file for other settings you may want to adjust
production: &base
@@ -43,6 +51,8 @@ production: &base
# email_enabled: true
# 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
@@ -56,16 +66,12 @@ production: &base
## COLOR = 5
# default_theme: 2 # default: 2
- # Restrict setting visibility levels for non-admin users.
- # The default is to allow all levels.
- # restricted_visibility_levels: [ "public" ]
-
## Automatic issue closing
# 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:
@@ -73,7 +79,6 @@ production: &base
merge_requests: true
wiki: true
snippets: false
- visibility_level: "private" # can be "private" | "internal" | "public"
## Webhook settings
# Number of seconds to wait for HTTP response after sending webhook HTTP POST request (default: 10)
@@ -84,35 +89,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:
@@ -131,6 +107,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
#
@@ -163,6 +148,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
@@ -288,6 +278,9 @@ production: &base
rack_attack:
git_basic_auth:
+ # Rack Attack IP banning enabled
+ # enabled: true
+ #
# Whitelist requests from 127.0.0.1 for web proxies (NGINX/Apache) with incorrect headers
# ip_whitelist: ["127.0.0.1"]
#
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 6a8bbb80b9c..e5ac66a2323 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'] ||= []
@@ -102,6 +107,8 @@ Settings.gitlab['relative_url_root'] ||= ENV['RAILS_RELATIVE_URL_ROOT'] || ''
Settings.gitlab['protocol'] ||= Settings.gitlab.https ? "https" : "http"
Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].nil?
Settings.gitlab['email_from'] ||= "gitlab@#{Settings.gitlab.host}"
+Settings.gitlab['email_display_name'] ||= "GitLab"
+Settings.gitlab['email_reply_to'] ||= "noreply@#{Settings.gitlab.host}"
Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
Settings.gitlab['user_home'] ||= begin
@@ -118,12 +125,14 @@ 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?
Settings.gitlab.default_projects_features['snippets'] = false if Settings.gitlab.default_projects_features['snippets'].nil?
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root)
+Settings.gitlab['restricted_signup_domains'] ||= []
#
# Gravatar
@@ -182,6 +191,7 @@ Settings['extra'] ||= Settingslogic.new({})
#
Settings['rack_attack'] ||= Settingslogic.new({})
Settings.rack_attack['git_basic_auth'] ||= Settingslogic.new({})
+Settings.rack_attack.git_basic_auth['enabled'] = true if Settings.rack_attack.git_basic_auth['enabled'].nil?
Settings.rack_attack.git_basic_auth['ip_whitelist'] ||= %w{127.0.0.1}
Settings.rack_attack.git_basic_auth['maxretry'] ||= 10
Settings.rack_attack.git_basic_auth['findtime'] ||= 1.minute
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/6_rack_profiler.rb b/config/initializers/6_rack_profiler.rb
index b6340287569..38a5fa98dc2 100644
--- a/config/initializers/6_rack_profiler.rb
+++ b/config/initializers/6_rack_profiler.rb
@@ -1,8 +1,9 @@
-if Rails.env == 'development'
+if Rails.env.development?
require 'rack-mini-profiler'
# initialization is skipped so trigger it
Rack::MiniProfilerRails.initialize!(Rails.application)
Rack::MiniProfiler.config.position = 'right'
Rack::MiniProfiler.config.start_hidden = true
+ Rack::MiniProfiler.config.skip_paths << '/specs'
end
diff --git a/config/initializers/8_default_url_options.rb b/config/initializers/8_default_url_options.rb
new file mode 100644
index 00000000000..8fd27b1d88e
--- /dev/null
+++ b/config/initializers/8_default_url_options.rb
@@ -0,0 +1,11 @@
+default_url_options = {
+ host: Gitlab.config.gitlab.host,
+ protocol: Gitlab.config.gitlab.protocol,
+ script_name: Gitlab.config.gitlab.relative_url_root
+}
+
+unless Gitlab.config.gitlab_on_standard_port?
+ default_url_options[:port] = Gitlab.config.gitlab.port
+end
+
+Rails.application.routes.default_url_options = default_url_options
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..8f8c4169740 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -2,13 +2,8 @@
# four configuration values can also be set straight in your models.
Devise.setup do |config|
# ==> Mailer Configuration
- # Configure the e-mail address which will be shown in Devise::Mailer,
- # note that it will be overwritten if you use your own mailer class with default "from" parameter.
- config.mailer_sender = "GitLab <#{Gitlab.config.gitlab.email_from}>"
-
-
# Configure the class responsible to send e-mails.
- # config.mailer = "Devise::Mailer"
+ config.mailer = "DeviseMailer"
# ==> ORM configuration
# Load and configure the ORM. Supports :active_record (default) and
@@ -208,7 +203,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/initializers/public_key.rb b/config/initializers/public_key.rb
index 75d74e3625d..e4f09a2d020 100644
--- a/config/initializers/public_key.rb
+++ b/config/initializers/public_key.rb
@@ -1,2 +1,2 @@
-path = File.expand_path("~/.ssh/id_rsa.pub")
+path = File.expand_path("~/.ssh/bitbucket_rsa.pub")
Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb
deleted file mode 100644
index bc88595cf26..00000000000
--- a/config/initializers/timeout.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# Slowpoke extends Rack::Timeout to gracefully kill Unicorn workers so they can clean up state.
-Slowpoke.timeout = 60
-
-# The `Rack::Timeout` middleware kills requests after 60 seconds (as set above).
-# We're replacing it with our `Gitlab::Middleware::Timeout` that does the same,
-# except ignoring Git-over-HTTP requests, letting those take as long as they need.
-
-Rails.application.config.middleware.swap(Rack::Timeout, Gitlab::Middleware::Timeout)
diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml
index 1cbcde5b3da..f3db5b7476e 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -23,8 +23,8 @@ en:
timeout: 'Your session expired, please sign in again to continue.'
inactive: 'Your account was not activated yet.'
sessions:
- signed_in: 'Signed in successfully.'
- signed_out: 'Signed out successfully.'
+ signed_in: ''
+ signed_out: ''
users_sessions:
user:
signed_in: 'Signed in successfully.'
@@ -57,4 +57,4 @@ en:
reset_password_instructions:
subject: 'Reset password instructions'
unlock_instructions:
- subject: 'Unlock Instructions'
+ subject: 'Unlock Instructions' \ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index 637b855e661..4b38dede7b4 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,12 +2,18 @@ require 'sidekiq/web'
require 'api/api'
Gitlab::Application.routes.draw do
+ mount JasmineRails::Engine => '/specs' if defined?(JasmineRails)
use_doorkeeper do
controllers applications: 'oauth/applications',
authorized_applications: 'oauth/authorized_applications',
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 +40,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'
@@ -46,8 +52,19 @@ Gitlab::Application.routes.draw do
get 'raw'
end
end
- get '/s/:username' => 'snippets#user_index', as: :user_snippets, constraints: { username: /.*/ }
+ get '/s/:username' => 'snippets#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 +93,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 +112,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
@@ -136,10 +162,12 @@ Gitlab::Application.routes.draw do
resources :groups, constraints: { id: /[^\/]+/ } do
member do
- put :project_teams_update
+ put :members_update
end
end
+ resources :deploy_keys, only: [:index, :show, :new, :create, :destroy]
+
resources :hooks, only: [:index, :create, :destroy] do
get :test
end
@@ -184,7 +212,11 @@ Gitlab::Application.routes.draw do
end
scope module: :profiles do
- resource :account, only: [:show, :update]
+ resource :account, only: [:show, :update] do
+ member do
+ delete :unlink
+ end
+ end
resource :notifications, only: [:show, :update]
resource :password, only: [:new, :create, :edit, :update] do
member do
@@ -198,17 +230,19 @@ Gitlab::Application.routes.draw do
end
get 'u/:username/calendar' => 'users#calendar', as: :user_calendar,
- constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
+ constraints: { username: /.*/ }
+
+ get 'u/:username/calendar_activities' => 'users#calendar_activities', as: :user_calendar_activities,
+ constraints: { username: /.*/ }
get '/u/:username' => 'users#show', as: :user,
- constraints: { username: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
#
# Dashboard Area
#
resource :dashboard, controller: 'dashboard', only: [:show] do
member do
- get :projects
get :issues
get :merge_requests
end
@@ -216,11 +250,7 @@ Gitlab::Application.routes.draw do
scope module: :dashboard do
resources :milestones, only: [:index, :show]
- resources :groups, only: [:index] do
- member do
- delete :leave
- end
- end
+ resources :groups, only: [:index]
resources :projects, only: [] do
collection do
@@ -233,16 +263,19 @@ Gitlab::Application.routes.draw do
#
# Groups Area
#
- resources :groups, constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ } do
+ resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
member do
get :issues
get :merge_requests
- get :members
get :projects
end
scope module: :groups do
- resources :group_members, only: [:create, :update, :destroy]
+ resources :group_members, only: [:index, :create, :update, :destroy] do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
+ end
+
resource :avatar, only: [:destroy]
resources :milestones, only: [:index, :show, :update]
end
@@ -262,7 +295,7 @@ Gitlab::Application.routes.draw do
# Project Area
#
resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+/ }, except:
+ resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
[:new, :create, :index], path: "/") do
member do
put :transfer
@@ -318,14 +351,6 @@ Gitlab::Application.routes.draw do
as: :tree
)
end
- resource :avatar, only: [:show, :destroy]
-
- resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do
- get :branches, on: :member
- end
-
- resources :commits, only: [:show], constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
- resources :compare, only: [:index, :create]
scope do
get(
@@ -336,8 +361,24 @@ Gitlab::Application.routes.draw do
)
end
- resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }
- resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do
+ scope do
+ get(
+ '/commits/*id',
+ to: 'commits#show',
+ constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
+ as: :commits
+ )
+ end
+
+ resource :avatar, only: [:show, :destroy]
+ resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do
+ get :branches, on: :member
+ end
+
+ resources :compare, only: [:index, :create]
+ resources :network, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ }
+
+ resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do
member do
get :commits
end
@@ -376,7 +417,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
@@ -394,7 +435,7 @@ Gitlab::Application.routes.draw do
member do
# tree viewer logs
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
- get 'logs_tree/:path' => 'refs#logs_tree', as: :logs_file, constraints: {
+ get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
id: Gitlab::Regex.git_reference_regex,
path: /.*/
}
@@ -407,6 +448,7 @@ Gitlab::Application.routes.draw do
post :automerge
get :automerge_check
get :ci_status
+ post :toggle_subscription
end
collection do
@@ -426,7 +468,6 @@ Gitlab::Application.routes.draw do
end
end
- resources :team, controller: 'team_members', only: [:index]
resources :milestones, except: [:destroy], constraints: { id: /\d+/ } do
member do
put :sort_issues
@@ -441,12 +482,15 @@ Gitlab::Application.routes.draw do
end
resources :issues, constraints: { id: /\d+/ }, except: [:destroy] do
+ member do
+ post :toggle_subscription
+ end
collection do
post :bulk_update
end
end
- resources :team_members, except: [:index, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
+ resources :project_members, except: [:new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ } do
collection do
delete :leave
@@ -455,6 +499,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
@@ -465,7 +513,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/config/unicorn.rb.example b/config/unicorn.rb.example
index 29253b71f49..86a5512e761 100644
--- a/config/unicorn.rb.example
+++ b/config/unicorn.rb.example
@@ -16,7 +16,7 @@
# Read about unicorn workers here:
# http://doc.gitlab.com/ee/install/requirements.html#unicorn-workers
#
-worker_processes 2
+worker_processes 3
# Since Unicorn is never exposed to outside clients, it does not need to
# run on the standard HTTP port (80), there is no reason to start Unicorn
@@ -35,10 +35,22 @@ working_directory "/home/git/gitlab" # available in 0.94.0+
listen "/home/git/gitlab/tmp/sockets/gitlab.socket", :backlog => 1024
listen "127.0.0.1:8080", :tcp_nopush => true
-# Kill workers after 1 hour.
-# A shorter timeout of 60 seconds is enforced by rack-timeout for web requests.
-# Git-over-HTTP only has the below timeout since large pulls/pushes can take a long time.
-timeout 60 * 60
+# nuke workers after 30 seconds instead of 60 seconds (the default)
+#
+# NOTICE: git push over http depends on this value.
+# If you want be able to push huge amount of data to git repository over http
+# you will have to increase this value too.
+#
+# Example of output if you try to push 1GB repo to GitLab over http.
+# -> git push http://gitlab.... master
+#
+# error: RPC failed; result=18, HTTP code = 200
+# fatal: The remote end hung up unexpectedly
+# fatal: The remote end hung up unexpectedly
+#
+# For more information see http://stackoverflow.com/a/21682112/752049
+#
+timeout 60
# feel free to point this anywhere accessible on the filesystem
pid "/home/git/gitlab/tmp/pids/unicorn.pid"
diff --git a/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb
new file mode 100644
index 00000000000..494c3033bff
--- /dev/null
+++ b/db/migrate/20150301014758_add_restricted_visibility_levels_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddRestrictedVisibilityLevelsToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :restricted_visibility_levels, :text
+ end
+end
diff --git a/db/migrate/20150306023106_fix_namespace_duplication.rb b/db/migrate/20150306023106_fix_namespace_duplication.rb
new file mode 100644
index 00000000000..334e5574559
--- /dev/null
+++ b/db/migrate/20150306023106_fix_namespace_duplication.rb
@@ -0,0 +1,21 @@
+class FixNamespaceDuplication < ActiveRecord::Migration
+ def up
+ #fixes path duplication
+ select_all('SELECT MAX(id) max, COUNT(id) cnt, path FROM namespaces GROUP BY path HAVING COUNT(id) > 1').each do |nms|
+ bad_nms_ids = select_all("SELECT id FROM namespaces WHERE path = '#{nms['path']}' AND id <> #{nms['max']}").map{|x| x["id"]}
+ execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})")
+ execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})")
+ end
+
+ #fixes name duplication
+ select_all('SELECT MAX(id) max, COUNT(id) cnt, name FROM namespaces GROUP BY name HAVING COUNT(id) > 1').each do |nms|
+ bad_nms_ids = select_all("SELECT id FROM namespaces WHERE name = '#{nms['name']}' AND id <> #{nms['max']}").map{|x| x["id"]}
+ execute("UPDATE projects SET namespace_id = #{nms["max"]} WHERE namespace_id IN(#{bad_nms_ids.join(', ')})")
+ execute("DELETE FROM namespaces WHERE id IN(#{bad_nms_ids.join(', ')})")
+ end
+ end
+
+ def down
+ # not implemented
+ end
+end
diff --git a/db/migrate/20150306023112_add_unique_index_to_namespace.rb b/db/migrate/20150306023112_add_unique_index_to_namespace.rb
new file mode 100644
index 00000000000..6472138e3ef
--- /dev/null
+++ b/db/migrate/20150306023112_add_unique_index_to_namespace.rb
@@ -0,0 +1,9 @@
+class AddUniqueIndexToNamespace < ActiveRecord::Migration
+ def change
+ remove_index :namespaces, column: :name if index_exists?(:namespaces, :name)
+ remove_index :namespaces, column: :path if index_exists?(:namespaces, :path)
+
+ add_index :namespaces, :name, unique: true
+ add_index :namespaces, :path, unique: true
+ end
+end
diff --git a/db/migrate/20150313012111_create_subscriptions_table.rb b/db/migrate/20150313012111_create_subscriptions_table.rb
new file mode 100644
index 00000000000..a1d4d9dedc5
--- /dev/null
+++ b/db/migrate/20150313012111_create_subscriptions_table.rb
@@ -0,0 +1,16 @@
+class CreateSubscriptionsTable < ActiveRecord::Migration
+ def change
+ create_table :subscriptions do |t|
+ t.integer :user_id
+ t.references :subscribable, polymorphic: true
+ t.boolean :subscribed
+
+ t.timestamps
+ end
+
+ add_index :subscriptions,
+ [:subscribable_id, :subscribable_type, :user_id],
+ unique: true,
+ name: 'subscriptions_user_id_and_ref_fields'
+ end
+end
diff --git a/db/migrate/20150320234437_add_location_to_user.rb b/db/migrate/20150320234437_add_location_to_user.rb
new file mode 100644
index 00000000000..32731d37d75
--- /dev/null
+++ b/db/migrate/20150320234437_add_location_to_user.rb
@@ -0,0 +1,5 @@
+class AddLocationToUser < ActiveRecord::Migration
+ def change
+ add_column :users, :location, :string
+ end
+end
diff --git a/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb
new file mode 100644
index 00000000000..42dc8173e46
--- /dev/null
+++ b/db/migrate/20150324155957_set_incorrect_assignee_id_to_null.rb
@@ -0,0 +1,6 @@
+class SetIncorrectAssigneeIdToNull < ActiveRecord::Migration
+ def up
+ execute "UPDATE issues SET assignee_id = NULL WHERE assignee_id = -1"
+ execute "UPDATE merge_requests SET assignee_id = NULL WHERE assignee_id = -1"
+ 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/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb
new file mode 100644
index 00000000000..3057ea3c68c
--- /dev/null
+++ b/db/migrate/20150421120000_remove_periods_at_ends_of_usernames.rb
@@ -0,0 +1,88 @@
+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
+ # Get the email username by removing everything after an `@` sign.
+ path.gsub!(/@.*\z/, "")
+ # Usernames can't end in .git, so remove it.
+ path.gsub!(/\.git\z/, "")
+ # Remove dashes at the start of the username.
+ path.gsub!(/\A-+/, "")
+ # Remove periods at the end of the username.
+ path.gsub!(/\.+\z/, "")
+ # Remove everything that's not in the list of allowed characters.
+ path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+
+ # Users with the great usernames of "." or ".." would end up with a blank username.
+ # Work around that by setting their username to "blank", followed by a counter.
+ path = "blank" if path.blank?
+
+ 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|
+ # Don't attempt to move if original path only contains periods.
+ next if path_was =~ /\A\.+\z/
+
+ 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/20150423033240_add_default_project_visibililty_to_application_settings.rb b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb
new file mode 100644
index 00000000000..9b0f13f3fa7
--- /dev/null
+++ b/db/migrate/20150423033240_add_default_project_visibililty_to_application_settings.rb
@@ -0,0 +1,7 @@
+class AddDefaultProjectVisibililtyToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :default_project_visibility, :integer
+ visibility = Settings.gitlab.default_projects_features['visibility_level']
+ execute("update application_settings set default_project_visibility = #{visibility}")
+ end
+end
diff --git a/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
new file mode 100644
index 00000000000..281c88d2a7d
--- /dev/null
+++ b/db/migrate/20150425164646_gitlab_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
@@ -0,0 +1,10 @@
+# This migration is a duplicate of 20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
+# It shold be applied before the index additions to ensure that `name` is case sensitive.
+
+class GitlabChangeCollationForTagNames < ActiveRecord::Migration
+ def up
+ if ActsAsTaggableOn::Utils.using_mysql?
+ execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
+ end
+ end
+end
diff --git a/db/migrate/20150425164647_remove_duplicate_tags.rb b/db/migrate/20150425164647_remove_duplicate_tags.rb
new file mode 100644
index 00000000000..1a9152cb965
--- /dev/null
+++ b/db/migrate/20150425164647_remove_duplicate_tags.rb
@@ -0,0 +1,16 @@
+class RemoveDuplicateTags < ActiveRecord::Migration
+ def up
+ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(id) > 1").each do |tag|
+ duplicate_ids = select_all("SELECT id FROM tags WHERE name = '#{tag["name"]}'").map{|tag| tag["id"]}
+ origin_tag_id = duplicate_ids.first
+ duplicate_ids.delete origin_tag_id
+
+ execute("UPDATE taggings SET tag_id = #{origin_tag_id} WHERE tag_id IN(#{duplicate_ids.join(",")})")
+ execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
+ end
+ end
+
+ def down
+
+ end
+end
diff --git a/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb
new file mode 100644
index 00000000000..c1b78681519
--- /dev/null
+++ b/db/migrate/20150425164648_add_missing_unique_indices.acts_as_taggable_on_engine.rb
@@ -0,0 +1,27 @@
+# This migration comes from acts_as_taggable_on_engine (originally 2)
+class AddMissingUniqueIndices < ActiveRecord::Migration
+ def self.up
+ add_index :tags, :name, unique: true
+
+ # pre-GitLab v6.7.0 may not have these indices since there were no
+ # migrations for them
+ if index_exists?(:taggings, :tag_id)
+ remove_index :taggings, :tag_id
+ end
+
+ if index_exists?(:taggings, [:taggable_id, :taggable_type, :context])
+ remove_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+ add_index :taggings,
+ [:tag_id, :taggable_id, :taggable_type, :context, :tagger_id, :tagger_type],
+ unique: true, name: 'taggings_idx'
+ end
+
+ def self.down
+ remove_index :tags, :name
+
+ remove_index :taggings, name: 'taggings_idx'
+ add_index :taggings, :tag_id
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+end
diff --git a/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
new file mode 100644
index 00000000000..8edb5080781
--- /dev/null
+++ b/db/migrate/20150425164649_add_taggings_counter_cache_to_tags.acts_as_taggable_on_engine.rb
@@ -0,0 +1,15 @@
+# This migration comes from acts_as_taggable_on_engine (originally 3)
+class AddTaggingsCounterCacheToTags < ActiveRecord::Migration
+ def self.up
+ add_column :tags, :taggings_count, :integer, default: 0
+
+ ActsAsTaggableOn::Tag.reset_column_information
+ ActsAsTaggableOn::Tag.find_each do |tag|
+ ActsAsTaggableOn::Tag.reset_counters(tag.id, :taggings)
+ end
+ end
+
+ def self.down
+ remove_column :tags, :taggings_count
+ end
+end
diff --git a/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb
new file mode 100644
index 00000000000..71f2d7f4330
--- /dev/null
+++ b/db/migrate/20150425164650_add_missing_taggable_index.acts_as_taggable_on_engine.rb
@@ -0,0 +1,10 @@
+# This migration comes from acts_as_taggable_on_engine (originally 4)
+class AddMissingTaggableIndex < ActiveRecord::Migration
+ def self.up
+ add_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+
+ def self.down
+ remove_index :taggings, [:taggable_id, :taggable_type, :context]
+ end
+end
diff --git a/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
new file mode 100644
index 00000000000..bfb06bc7cda
--- /dev/null
+++ b/db/migrate/20150425164651_change_collation_for_tag_names.acts_as_taggable_on_engine.rb
@@ -0,0 +1,10 @@
+# This migration comes from acts_as_taggable_on_engine (originally 5)
+# This migration is added to circumvent issue #623 and have special characters
+# work properly
+class ChangeCollationForTagNames < ActiveRecord::Migration
+ def up
+ if ActsAsTaggableOn::Utils.using_mysql?
+ execute("ALTER TABLE tags MODIFY name varchar(255) CHARACTER SET utf8 COLLATE utf8_bin;")
+ end
+ end
+end
diff --git a/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb
new file mode 100644
index 00000000000..51237354d9f
--- /dev/null
+++ b/db/migrate/20150425173433_add_default_snippet_visibility_to_app_settings.rb
@@ -0,0 +1,7 @@
+class AddDefaultSnippetVisibilityToAppSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :default_snippet_visibility, :integer
+ visibility = Settings.gitlab.default_projects_features['visibility_level']
+ execute("update application_settings set default_snippet_visibility = #{visibility}")
+ end
+end
diff --git a/db/migrate/20150429002313_remove_abandoned_group_members_records.rb b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb
new file mode 100644
index 00000000000..6013605bb35
--- /dev/null
+++ b/db/migrate/20150429002313_remove_abandoned_group_members_records.rb
@@ -0,0 +1,6 @@
+class RemoveAbandonedGroupMembersRecords < ActiveRecord::Migration
+ def change
+ execute("DELETE FROM members WHERE type = 'GroupMember' AND source_id NOT IN(\
+ SELECT id FROM namespaces WHERE type='Group')")
+ end
+end
diff --git a/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb
new file mode 100644
index 00000000000..184e2653610
--- /dev/null
+++ b/db/migrate/20150502064022_add_restricted_signup_domains_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddRestrictedSignupDomainsToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :restricted_signup_domains, :text
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d63e1bc17a9..1fe092ec5d2 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: 20150310194358) do
+ActiveRecord::Schema.define(version: 20150502064022) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -25,9 +25,14 @@ ActiveRecord::Schema.define(version: 20150310194358) do
t.datetime "created_at"
t.datetime "updated_at"
t.string "home_page_url"
- t.integer "default_branch_protection", default: 2
- t.boolean "twitter_sharing_enabled", default: true
- t.boolean "version_check_enabled", default: true
+ t.integer "default_branch_protection", default: 2
+ t.boolean "twitter_sharing_enabled", default: true
+ t.text "restricted_visibility_levels"
+ t.boolean "version_check_enabled", default: true
+ t.integer "max_attachment_size", default: 10, null: false
+ t.integer "default_project_visibility"
+ t.integer "default_snippet_visibility"
+ t.text "restricted_signup_domains"
end
create_table "broadcast_messages", force: true do |t|
@@ -131,6 +136,7 @@ ActiveRecord::Schema.define(version: 20150310194358) 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 +167,20 @@ ActiveRecord::Schema.define(version: 20150310194358) 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
@@ -243,9 +254,9 @@ ActiveRecord::Schema.define(version: 20150310194358) do
end
add_index "namespaces", ["created_at", "id"], name: "index_namespaces_on_created_at_and_id", using: :btree
- add_index "namespaces", ["name"], name: "index_namespaces_on_name", using: :btree
+ add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
- add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
+ add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "notes", force: true do |t|
@@ -316,6 +327,11 @@ ActiveRecord::Schema.define(version: 20150310194358) 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"
@@ -398,6 +414,17 @@ ActiveRecord::Schema.define(version: 20150310194358) do
add_index "snippets", ["project_id"], name: "index_snippets_on_project_id", using: :btree
add_index "snippets", ["visibility_level"], name: "index_snippets_on_visibility_level", using: :btree
+ create_table "subscriptions", force: true do |t|
+ t.integer "user_id"
+ t.integer "subscribable_id"
+ t.string "subscribable_type"
+ t.boolean "subscribed"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "subscriptions", ["subscribable_id", "subscribable_type", "user_id"], name: "subscriptions_user_id_and_ref_fields", unique: true, using: :btree
+
create_table "taggings", force: true do |t|
t.integer "tag_id"
t.integer "taggable_id"
@@ -408,13 +435,16 @@ ActiveRecord::Schema.define(version: 20150310194358) do
t.datetime "created_at"
end
- add_index "taggings", ["tag_id"], name: "index_taggings_on_tag_id", using: :btree
+ add_index "taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true, using: :btree
add_index "taggings", ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
create_table "tags", force: true do |t|
- t.string "name"
+ t.string "name"
+ t.integer "taggings_count", default: 0
end
+ add_index "tags", ["name"], name: "index_tags_on_name", unique: true, using: :btree
+
create_table "users", force: true do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@@ -462,6 +492,8 @@ ActiveRecord::Schema.define(version: 20150310194358) do
t.boolean "password_automatically_set", default: false
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/README.md b/doc/api/README.md
index dec530d0b81..f6757b0a6aa 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -6,6 +6,7 @@
- [Session](session.md)
- [Projects](projects.md)
- [Project Snippets](project_snippets.md)
+- [Services](services.md)
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Commits](commits.md)
diff --git a/doc/api/groups.md b/doc/api/groups.md
index b5a4b05ccaf..c903a850fdd 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -35,7 +35,7 @@ Parameters:
## New group
-Creates a new project group. Available only for admin.
+Creates a new project group. Available only for users who can create groups.
```
POST /groups
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a7dd8b74c35..d407bc35d79 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -99,11 +99,13 @@ GET /projects/:id/issues?labels=foo,bar
GET /projects/:id/issues?labels=foo,bar&state=opened
GET /projects/:id/issues?milestone=1.0.0
GET /projects/:id/issues?milestone=1.0.0&state=opened
+GET /projects/:id/issues?iid=42
```
Parameters:
- `id` (required) - The ID of a project
+- `iid` (optional) - Return the issue having the given `iid`
- `state` (optional) - Return `all` issues or just those that are `opened` or `closed`
- `labels` (optional) - Comma-separated list of label names
- `milestone` (optional) - Milestone title
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 1f3fd26a241..c1d82ad9576 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -10,11 +10,13 @@ The pagination parameters `page` and `per_page` can be used to restrict the list
GET /projects/:id/merge_requests
GET /projects/:id/merge_requests?state=opened
GET /projects/:id/merge_requests?state=all
+GET /projects/:id/merge_requests?iid=42
```
Parameters:
- `id` (required) - The ID of a project
+- `iid` (optional) - Return the request having the given `iid`
- `state` (optional) - Return `all` requests or just those that are `merged`, `opened` or `closed`
- `order_by` (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
@@ -375,7 +377,7 @@ Parameters:
}
},
{
- "note": "_Status changed to closed_",
+ "note": "Status changed to closed",
"author": {
"id": 11,
"username": "admin",
@@ -388,6 +390,6 @@ Parameters:
]
```
-## Comments on issues
+## Comments on merge requets
Comments are done via the notes resource.
diff --git a/doc/api/milestones.md b/doc/api/milestones.md
index d48b3bcce8a..a6828728264 100644
--- a/doc/api/milestones.md
+++ b/doc/api/milestones.md
@@ -6,6 +6,7 @@ Returns a list of project milestones.
```
GET /projects/:id/milestones
+GET /projects/:id/milestones?iid=42
```
```json
@@ -27,6 +28,7 @@ GET /projects/:id/milestones
Parameters:
- `id` (required) - The ID of a project
+- `iid` (optional) - Return the milestone having the given `iid`
## Get single milestone
diff --git a/doc/api/notes.md b/doc/api/notes.md
index c22e493562a..ee2f9fa0eac 100644
--- a/doc/api/notes.md
+++ b/doc/api/notes.md
@@ -21,7 +21,7 @@ Parameters:
[
{
"id": 302,
- "body": "_Status changed to closed_",
+ "body": "Status changed to closed",
"attachment": null,
"author": {
"id": 1,
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index 50e134847c0..a7acf37b5bc 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -1,5 +1,18 @@
# Project snippets
+### Snippet visibility level
+
+Snippets in GitLab can be either private, internal or public.
+You can set it with the `visibility_level` field in the snippet.
+
+Constants for snippet visibility levels are:
+
+| Visibility | visibility_level | Description |
+| ---------- | ---------------- | ----------- |
+| Private | `0` | The snippet is visible only the snippet creator |
+| Internal | `10` | The snippet is visible for any logged in user |
+| Public | `20` | The snippet can be accessed without any authentication |
+
## List snippets
Get a list of project snippets.
@@ -58,6 +71,7 @@ Parameters:
- `title` (required) - The title of a snippet
- `file_name` (required) - The name of a snippet file
- `code` (required) - The content of a snippet
+- `visibility_level` (required) - The snippet's visibility
## Update snippet
@@ -74,6 +88,7 @@ Parameters:
- `title` (optional) - The title of a snippet
- `file_name` (optional) - The name of a snippet file
- `code` (optional) - The content of a snippet
+- `visibility_level` (optional) - The snippet's visibility
## Delete snippet
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/api/users.md b/doc/api/users.md
index a8b7685b503..cd141daadc8 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -57,7 +57,8 @@ GET /users
"color_scheme_id": 2,
"is_admin": false,
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
- "can_create_group": true
+ "can_create_group": true,
+ "current_sign_in_at": "2014-03-19T13:12:15Z"
},
{
"id": 2,
@@ -79,7 +80,8 @@ GET /users
"avatar_url": "http://localhost:3000/uploads/user/avatar/1/cd8.jpeg",
"can_create_group": true,
"can_create_project": true,
- "projects_limit": 100
+ "projects_limit": 100,
+ "current_sign_in_at": "2014-03-19T17:54:13Z"
}
]
```
diff --git a/doc/customization/issue_closing.md b/doc/customization/issue_closing.md
index ddc0c8eac2b..64f128f5a63 100644
--- a/doc/customization/issue_closing.md
+++ b/doc/customization/issue_closing.md
@@ -1,5 +1,38 @@
# Issue closing pattern
-By default you can close issues from commit messages by saying 'Closes #12' or 'Fixed #101'.
+Here's how to close multiple issues in one commit message:
-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)
+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.
+
+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 \ No newline at end of file
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 714cc016004..541af487bb1 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -54,7 +54,7 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
![GitLab Diagram Overview](gitlab_diagram_overview.png)
-A typical install of GitLab will be on Ubuntu Linux or RHEL/CentOS. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
+A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository. `/home/git/gitlab-satellites` keeps checked out repositories when performing actions such as a merge request, editing files in the web interface, etc.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 2b204c72476..ca25eaea799 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -4,6 +4,12 @@
Since an installation from source is a lot of work and error prone we strongly recommend the fast and reliable [Omnibus package installation](https://about.gitlab.com/downloads/) (deb/rpm).
+One reason the Omnibus package is more reliable is its use of Runit to restart any of the GitLab processes in case one crashes.
+On heavily used GitLab instances the memory usage of the Sidekiq background worker will grow over time.
+Omnibus packages solve this by [letting the Sidekiq terminate gracefully](http://doc.gitlab.com/ce/operations/sidekiq_memory_killer.html) if it uses too much memory.
+After this termination Runit will detect Sidekiq is not running and will start it.
+Since installations from source don't have Runit, Sidekiq can't be terminated and its memory usage will grow over time.
+
## Select Version to Install
Make sure you view [this installation guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md) from the tag (version) of GitLab you would like to install.
@@ -56,7 +62,7 @@ up-to-date and install it.
Install the required packages (needed to compile Ruby and native extensions to Ruby gems):
- sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev
+ sudo apt-get install -y build-essential zlib1g-dev libyaml-dev libssl-dev libgdbm-dev libreadline-dev libncurses5-dev libffi-dev curl openssh-server redis-server checkinstall libxml2-dev libxslt-dev libcurl4-openssl-dev libicu-dev logrotate python-docutils pkg-config cmake libkrb5-dev nodejs
Make sure you have the right version of Git installed
@@ -103,8 +109,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 +189,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-8-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-8-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 +286,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.5.4] 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/install/requirements.md b/doc/install/requirements.md
index 5bdb9caa2bf..7a3216dd2d2 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -76,15 +76,18 @@ Notice: The 25 workers of Sidekiq will show up as separate processes in your pro
## Unicorn Workers
-It's possible to increase the amount of unicorn workers and tis will usually help for to reduce the response time of the applications.
+It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests.
+
For most instances we recommend using: CPU cores + 1 = unicorn workers.
So for a machine with 2 cores, 3 unicorn workers is ideal.
-For all machines that have 1GB and up we recommend a minimum of two unicorn workers.
+For all machines that have 1GB and up we recommend a minimum of three unicorn workers.
If you have a 512MB machine with a magnetic (non-SSD) swap drive we recommend to configure only one Unicorn worker to prevent excessive swapping.
With one Unicorn worker only git over ssh access will work because the git over HTTP access requires two running workers (one worker to receive the user request and one worker for the authorization check).
If you have a 512MB machine with a SSD drive you can use two Unicorn workers, this will allow HTTP access although it will be slow due to swapping.
+To change the Unicorn workers when you have the Omnibus package please see [the Unicorn settings in the Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/unicorn.md#unicorn-settings).
+
## Database
If you want to run the database separately, the **recommended** database size is **1 MB per user**.
@@ -103,4 +106,4 @@ On a very active server (10,000 active users) the Sidekiq process can use 1GB+ o
- Firefox (Latest released version and [latest ESR version](https://www.mozilla.org/en-US/firefox/organizations/))
- Safari 7+ (known problem: required fields in html5 do not work)
- Opera (Latest released version)
-- IE 10+
+- IE 10+ \ No newline at end of file
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index cc6389f5aaf..d82e1f8b41b 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -2,7 +2,7 @@
Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account.
-To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
+To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
Bitbucket will generate an application ID and secret key for you to use.
1. Sign in to Bitbucket.
@@ -19,8 +19,8 @@ Bitbucket will generate an application ID and secret key for you to use.
- URL: The URL to your GitLab installation. 'https://gitlab.company.com'
1. Select "Save".
-1. You should now see a Key and Secret in the list of OAuth customers.
- Keep this page open as you continue configuration.
+1. You should now see a Key and Secret in the list of OAuth customers.
+ Keep this page open as you continue configuration.
1. On your GitLab server, open the configuration file.
@@ -70,13 +70,13 @@ Bitbucket will generate an application ID and secret key for you to use.
1. Restart GitLab for the changes to take effect.
-On the sign in page there should now be a Bitbucket icon below the regular sign in form.
-Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
+On the sign in page there should now be a Bitbucket icon below the regular sign in form.
+Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
If everything goes well the user will be returned to GitLab and will be signed in.
## Bitbucket project import
-To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com.
+To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com.
Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key.
@@ -95,7 +95,7 @@ To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org
```sh
The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established.
RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
- Are you sure you want to continue connecting (yes/no)?
+ Are you sure you want to continue connecting (yes/no)?
```
1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts.
@@ -104,7 +104,7 @@ To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org
### Step 2: Public key
-To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/id_rsa.pub`, which will expand to `/home/git/.ssh/id_rsa.pub` in most configurations.
+To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations.
If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following:
@@ -114,6 +114,7 @@ If you have that file in place, you're all set and should see the "Import projec
sudo -u git -H ssh-keygen
```
+ When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`.
Make sure to use an **empty passphrase**.
2. Restart GitLab to allow it to find the new public key.
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 96755707dee..3e660cfba1e 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -36,4 +36,9 @@ In GitLab Admin section, navigate to `Service Templates` and choose the service
After the template is created, the template details will be pre-filled on the project service page.
+NOTE: For each project, you will still need to configure the issue tracking URLs by replacing `:issues_tracker_id` in the above screenshot
+with the ID used by your external issue tracker. Prior to GitLab v7.8, this ID was configured in the project settings, and GitLab would automatically
+update the URL configured in `gitlab.yml`. This behavior is now depecated, and all issue tracker URLs must be configured directly
+within the project's Services settings.
+
Support to add your commits to the Jira ticket automatically is [available in GitLab EE](http://doc.gitlab.com/ee/integration/jira.html).
diff --git a/doc/integration/gitlab_buttons_in_gmail.md b/doc/integration/gitlab_buttons_in_gmail.md
index a9885cef109..e35bb8ba693 100644
--- a/doc/integration/gitlab_buttons_in_gmail.md
+++ b/doc/integration/gitlab_buttons_in_gmail.md
@@ -7,11 +7,11 @@ If correctly setup, emails that require an action will be marked in Gmail.
![gitlab_actions](gitlab_actions.png)
To get this functioning, you need to be registered with Google.
-[See how to register with google in this document.](https://developers.google.com/gmail/markup/registering-with-google)
+[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google)
-To aid the registering with google, GitLab offers a rake task that will send an email to google whitelisting email address from your GitLab server.
+To aid the registering with Google, GitLab offers a rake task that will send an email to Google whitelisting email address from your GitLab server.
-To check what would be sent to the google email address, run the rake task:
+To check what would be sent to the Google email address, run the rake task:
```bash
bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production
@@ -19,7 +19,7 @@ bundle exec rake gitlab:mail_google_schema_whitelisting RAILS_ENV=production
**This will not send the email but give you the output of how the mail will look.**
-Copy the output of the rake task to [google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate".
+Copy the output of the rake task to [Google email markup tester](https://www.google.com/webmasters/markup-tester/u/0/) and press "Validate".
If you receive "No errors detected" message from the tester you can send the email using:
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/integration/slack.md b/doc/integration/slack.md
index 2fd22c513ad..84f1d74c058 100644
--- a/doc/integration/slack.md
+++ b/doc/integration/slack.md
@@ -16,7 +16,7 @@ To enable Slack integration you must create an Incoming WebHooks integration on
1. Choose the channel name you want to send notifications to
-1. Click **Add Incoming WebHooks Integration**Add Integrations.
+1. Click **Add Incoming WebHooks Integration**
- Optional step; You can change bot's name and avatar by clicking modifying the bot name or avatar under **Integration Settings**.
1. Copy the **Webhook URL**, we'll need this later for GitLab.
@@ -32,10 +32,15 @@ After Slack is ready we need to setup GitLab. Here are the steps to achieve this
1. Navigate to Settings -> Services -> Slack
-1. Fill in your Slack details
+1. Pick the triggers you want to activate
+1. Fill in your Slack details
+ - Webhook: Paste the Webhook URL from the step above
+ - Username: Fill this in if you want to change the username of the bot
+ - Channel: Fill this in if you want to change the channel where the messages will be posted
- Mark it as active
- - Paste in the webhook URL you got from Slack
+
+1. Save your settings
Have fun :)
diff --git a/doc/logs/logs.md b/doc/logs/logs.md
index ec0109a426f..83c32b09253 100644
--- a/doc/logs/logs.md
+++ b/doc/logs/logs.md
@@ -5,7 +5,7 @@ In addition to system log files, GitLab Enterprise Edition comes with Audit Even
System log files are typically plain text in a standard log file format. This guide talks about how to read and use these system log files.
#### production.log
-This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/logs/production.log` for installations from the source.
+This file lives in `/var/log/gitlab/gitlab-rails/production.log` for omnibus package or in `/home/git/gitlab/log/production.log` for installations from the source.
This file contains information about all performed requests. You can see url and type of request, IP address and what exactly parts of code were involved to service this particular request. Also you can see all SQL request that have been performed and how much time it took.
This task is more useful for GitLab contributors and developers. Use part of this log file when you are going to report bug.
@@ -30,7 +30,7 @@ Completed 200 OK in 166ms (Views: 117.4ms | ActiveRecord: 27.2ms)
In this example we can see that server processed HTTP request with url `/gitlabhq/yaml_db/tree/master` from IP 168.111.56.1 at 2015-02-12 19:34:53 +0200. Also we can see that request was processed by Projects::TreeController.
#### application.log
-This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/logs/application.log` for installations from the source.
+This file lives in `/var/log/gitlab/gitlab-rails/application.log` for omnibus package or in `/home/git/gitlab/log/application.log` for installations from the source.
This log file helps you discover events happening in your instance such as user creation, project removing and so on.
@@ -42,7 +42,7 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk) was
October 07, 2014 11:25: Project "project133" was removed
```
#### githost.log
-This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/logs/githost.log` for installations from the source.
+This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for omnibus package or in `/home/git/gitlab/log/githost.log` for installations from the source.
The GitLab has to interact with git repositories but in some rare cases something can go wrong and in this case you will know what exactly happened. This log file contains all failed requests from GitLab to git repository. In majority of cases this file will be useful for developers only.
```
@@ -52,7 +52,7 @@ error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/reposit
```
#### satellites.log
-This file lives in `/var/log/gitlab/gitlab-rails/satellites.log` for omnibus package or in `/home/git/gitlab/logs/satellites.log` for installations from the source.
+This file lives in `/var/log/gitlab/gitlab-rails/satellites.log` for omnibus package or in `/home/git/gitlab/log/satellites.log` for installations from the source.
In some cases GitLab should perform write actions to git repository, for example when it is needed to merge the merge request or edit a file with online editor. If something went wrong you can look into this file to find out what exactly happened.
```
@@ -62,7 +62,7 @@ October 07, 2014 11:36: PID: 1872: -> fatal: repository '/Users/vsizov/gitlab-de
```
#### sidekiq.log
-This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/logs/sidekiq.log` for installations from the source.
+This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for omnibus package or in `/home/git/gitlab/log/sidekiq.log` for installations from the source.
GitLab uses background jobs for processing tasks which can take a long time. All information about processing these jobs are writing down to this file.
```
@@ -71,7 +71,7 @@ GitLab uses background jobs for processing tasks which can take a long time. All
```
#### gitlab-shell.log
-This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/logs/sidekiq.log` for installations from the source.
+This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for installations from the source.
gitlab-shell is using by Gitlab for executing git commands and provide ssh access to git repositories.
@@ -81,7 +81,7 @@ I, [2015-02-13T06:17:00.679433 #9291] INFO -- : Moving existing hooks directory
```
#### unicorn_stderr.log
-This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/logs/unicorn_stderr.log` for installations from the source.
+This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for installations from the source.
Unicorn is a high-performance forking Web server which is used for serving GitLab application. You can look at this log, for example, if your application does not respond. This log cantains all information about state of unicorn processes at any given time.
@@ -99,4 +99,4 @@ W, [2015-02-13T07:16:01.313000 #9094] WARN -- : Unicorn::WorkerKiller send SIGQ
I, [2015-02-13T07:16:01.530733 #9047] INFO -- : reaped #<Process::Status: pid 9094 exit 0> worker=1
I, [2015-02-13T07:16:01.534501 #13379] INFO -- : worker=1 spawned pid=13379
I, [2015-02-13T07:16:01.534848 #13379] INFO -- : worker=1 ready
-```
+``` \ No newline at end of file
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index 1096ea9656c..e95ddbb7578 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -6,7 +6,7 @@
* [Newlines](#newlines)
* [Multiple underscores in words](#multiple-underscores-in-words)
-* [URL auto-linking](#url-autolinking)
+* [URL auto-linking](#url-auto-linking)
* [Code and Syntax Highlighting](#code-and-syntax-highlighting)
* [Emoji](#emoji)
* [Special GitLab references](#special-gitlab-references)
@@ -46,14 +46,15 @@ You can also use other rich text files in GitLab. You might have to install a de
GFM honors the markdown specification in how [paragraphs and line breaks are handled](http://daringfireball.net/projects/markdown/syntax#p).
-A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.:
+A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
+Line-breaks, or softreturns, are rendered if you end a line with two or more spaces
- Roses are red
+ Roses are red [followed by two or more spaces]
Violets are blue
Sugar is sweet
-Roses are red
+Roses are red
Violets are blue
Sugar is sweet
@@ -140,29 +141,29 @@ But let's throw in a <b>tag</b>.
## Emoji
- Sometimes you want to be a :ninja: and add some :glowing_star: to your :speech_balloon:. Well we have a gift for you:
+ Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
- :high_voltage_sign: You can use emoji anywhere GFM is supported. :victory_hand:
+ :zap: You can use emoji anywhere GFM is supported. :v:
- You can use it to point out a :bug: or warn about :speak_no_evil_monkey: patches. And if someone improves your really :snail: code, send them some :cake:. People will :heart: you for that.
+ You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
- If you are new to this, don't be :fearful_face:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+ If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
- Consult the [Emoji Cheat Sheet](https://s3.amazonaws.com/emoji-cheatsheet/cheat_sheet.pdf) for a list of all supported emoji codes. :thumbsup:
+ Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
-Sometimes you want to be a :ninja: and add some :glowing_star: to your :speech_balloon:. Well we have a gift for you:
+Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
-:high_voltage_sign: You can use emoji anywhere GFM is supported. :victory_hand:
+:zap: You can use emoji anywhere GFM is supported. :v:
-You can use it to point out a :bug: or warn about :speak_no_evil_monkey: patches. And if someone improves your really :snail: code, send them some :cake:. People will :heart: you for that.
+You can use it to point out a :bug: or warn about :speak_no_evil: patches. And if someone improves your really :snail: code, send them some :birthday:. People will :heart: you for that.
-If you are new to this, don't be :fearful_face:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
+If you are new to this, don't be :fearful:. You can easily join the emoji :family:. All you need to do is to look up on the supported codes.
-Consult the [Emoji Cheat Sheet](https://s3.amazonaws.com/emoji-cheatsheet/cheat_sheet.pdf) for a list of all supported emoji codes. :thumbsup:
+Consult the [Emoji Cheat Sheet](http://emoji.codes) for a list of all supported emoji codes. :thumbsup:
## Special GitLab References
-GFM recognized special references.
+GFM recognizes special references.
You can easily reference e.g. an issue, a commit, a team member or even the whole team within a project.
@@ -170,31 +171,50 @@ GFM will turn that reference into a link so you can navigate between them easily
GFM will recognize the following:
-- @foo : for specific team members or groups
-- @all : for the whole team
-- #123 : for issues
-- !123 : for merge requests
-- $123 : for snippets
-- 1234567 : for commits
-- \[file\](path/to/file) : for file references
-
-GFM also recognizes references to commits, issues, and merge requests in other projects:
-
-- namespace/project#123 : for issues
-- namespace/project!123 : for merge requests
-- namespace/project@1234567 : for commits
+| input | references |
+|:-----------------------|:---------------------------|
+| `@user_name` | specific user |
+| `@group_name` | specific group |
+| `@all` | entire team |
+| `#123` | issue |
+| `!123` | merge request |
+| `$123` | snippet |
+| `~123` | label by ID |
+| `~bug` | one-word label by name |
+| `~"feature request"` | multi-word label by name |
+| `9ba12248` | specific commit |
+| `9ba12248...b19a04f5` | commit range comparison |
+| `[README](doc/README)` | repository file references |
+
+GFM also recognizes certain cross-project references:
+
+| input | references |
+|:----------------------------------------|:------------------------|
+| `namespace/project#123` | issue |
+| `namespace/project!123` | merge request |
+| `namespace/project$123` | snippet |
+| `namespace/project@9ba12248` | specific commit |
+| `namespace/project@9ba12248...b19a04f5` | commit range comparison |
## Task Lists
-You can add task lists to merge request and issue descriptions to keep track of to-do items. To create a task, add an unordered list to the description in an issue or merge request, formatted like so:
+You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
```no-highlight
-* [x] Completed task
-* [ ] Unfinished task
- * [x] Nested task
+- [x] Completed task
+- [ ] Incomplete task
+ - [ ] Sub-task 1
+ - [x] Sub-task 2
+ - [ ] Sub-task 3
```
-Task lists can only be created in descriptions, not in titles or comments. Task item state can be managed by editing the description's Markdown or by clicking the rendered checkboxes.
+- [x] Completed task
+- [ ] Incomplete task
+ - [ ] Sub-task 1
+ - [x] Sub-task 2
+ - [ ] Sub-task 3
+
+Task lists can only be created in descriptions, not in titles. Task item state can be managed by editing the description's Markdown or by toggling the rendered check boxes.
# Standard Markdown
@@ -234,51 +254,38 @@ Alt-H2
### Header IDs and links
-All markdown rendered headers automatically get IDs, except for comments.
+All Markdown-rendered headers automatically get IDs, except in comments.
On hover a link to those IDs becomes visible to make it easier to copy the link to the header to give it to someone else.
The IDs are generated from the content of the header according to the following rules:
-1. remove the heading hashes `#` and process the rest of the line as it would be processed if it were not a header
-2. from the result, remove all HTML tags, but keep their inner content
-3. convert all characters to lowercase
-4. convert all characters except `[a-z0-9_-]` into hyphens `-`
-5. transform multiple adjacent hyphens into a single hyphen
-6. remove trailing and heading hyphens
+1. All text is converted to lowercase
+1. All non-word text (e.g., punctuation, HTML) is removed
+1. All spaces are converted to hyphens
+1. Two or more hyphens in a row are converted to one
+1. If a header with the same ID has already been generated, a unique
+ incrementing number is appended.
For example:
```
-###### ..Ab_c-d. e [anchor](URL) ![alt text](URL)..
+# This header has spaces in it
+## This header has a :thumbsup: in it
+# This header has Unicode in it: 한글
+## This header has spaces in it
+### This header has spaces in it
```
-which renders as:
-
-###### ..Ab_c-d. e [anchor](URL) ![alt text](URL)..
+Would generate the following link IDs:
-will first be converted by step 1) into a string like:
-
-```
-..Ab_c-d. e &lt;a href="URL">anchor&lt;/a> &lt;img src="URL" alt="alt text"/>..
-```
+1. `this-header-has-spaces-in-it`
+1. `this-header-has-a-in-it`
+1. `this-header-has-unicode-in-it-한글`
+1. `this-header-has-spaces-in-it-1`
+1. `this-header-has-spaces-in-it-2`
-After removing the tags in step 2) we get:
-
-```
-..Ab_c-d. e anchor ..
-```
-
-And applying all the other steps gives the id:
-
-```
-ab_c-d-e-anchor
-```
-
-Note in particular how:
-
-- for markdown anchors `[text](URL)`, only the `text` is used
-- markdown images `![alt](URL)` are completely ignored
+Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
## Emphasis
@@ -310,8 +317,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
1. Ordered sub-list
4. And another item.
- Some text that should be aligned with the above item.
-
* Unordered list can use asterisks
- Or minuses
+ Or pluses
@@ -324,8 +329,6 @@ Strikethrough uses two tildes. ~~Scratch this.~~
1. Ordered sub-list
4. And another item.
- Some text that should be aligned with the above item.
-
* Unordered list can use asterisks
- Or minuses
+ Or pluses
@@ -420,7 +423,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 `span` elements, as well as the `class`, and `id` attributes on all elements.
```no-highlight
<dl>
@@ -485,6 +488,10 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
+
+This line is also a separate paragraph, and...
+This line is on its own line, because the previous line ends with two
+spaces.
```
Here's a line for us to start with.
@@ -494,6 +501,10 @@ This line is separated from the one above by two newlines, so it will be a *sepa
This line is also begins a separate paragraph, but...
This line is only separated by a single newline, so it's a separate line in the *same paragraph*.
+This line is also a separate paragraph, and...
+This line is on its own line, because the previous line ends with two
+spaces.
+
## Tables
Tables aren't part of the core Markdown spec, but they are part of GFM and Markdown Here supports them.
@@ -516,6 +527,20 @@ Code above produces next output:
The row of dashes between the table header and body must have at least three dashes in each column.
+By including colons in the header row, you can align the text within that column:
+
+```
+| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
+| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
+| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
+| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
+```
+
+| Left Aligned | Centered | Right Aligned | Left Aligned | Centered | Right Aligned |
+| :----------- | :------: | ------------: | :----------- | :------: | ------------: |
+| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
+| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
+
## References
- This document leveraged heavily from the [Markdown-Cheatsheet](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet).
diff --git a/doc/operations/sidekiq_memory_killer.md b/doc/operations/sidekiq_memory_killer.md
index 867b01b0d5a..811c2192a19 100644
--- a/doc/operations/sidekiq_memory_killer.md
+++ b/doc/operations/sidekiq_memory_killer.md
@@ -36,3 +36,5 @@ The MemoryKiller is controlled using environment variables.
Existing jobs get 30 seconds to finish. After that, the MemoryKiller tells
Sidekiq to shut down, and an external supervision mechanism (e.g. Runit) must
restart Sidekiq.
+- `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL`: defaults to 'SIGTERM'. The name of
+ the final signal sent to the Sidekiq process when we want it to shut down.
diff --git a/doc/permissions/permissions.md b/doc/permissions/permissions.md
index c9928e11b2e..8cfa7f9c876 100644
--- a/doc/permissions/permissions.md
+++ b/doc/permissions/permissions.md
@@ -41,6 +41,11 @@ If a user is a GitLab administrator they receive all permissions.
## Group
+In order for a group to appear as public and be browsable, it must contain at
+least one public project.
+
+Any user can remove themselves from a group, unless they are the last Owner of the group.
+
| Action | Guest | Reporter | Developer | Master | Owner |
|-------------------------|-------|----------|-----------|--------|-------|
| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
@@ -48,5 +53,3 @@ If a user is a GitLab administrator they receive all permissions.
| Create project in group | | | | ✓ | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
-
-Any user can remove themselves from a group, unless they are the last Owner of the group.
diff --git a/doc/project_services/hipchat.md b/doc/project_services/hipchat.md
new file mode 100644
index 00000000000..021a93a288f
--- /dev/null
+++ b/doc/project_services/hipchat.md
@@ -0,0 +1,54 @@
+# Atlassian HipChat
+
+GitLab provides a way to send HipChat notifications upon a number of events,
+such as when a user pushes code, creates a branch or tag, adds a comment, and
+creates a merge request.
+
+## Setup
+
+GitLab requires the use of a HipChat v2 API token to work. v1 tokens are
+not supported at this time. Note the differences between v1 and v2 tokens:
+
+HipChat v1 API (legacy) supports "API Auth Tokens" in the Group API menu. A v1
+token is allowed to send messages to *any* room.
+
+HipChat v2 API has tokens that are can be created using the Integrations tab
+in the Group or Room admin page. By design, these are lightweight tokens that
+allow GitLab to send messages only to *one* room.
+
+### Complete these steps in HipChat:
+
+1. Go to: https://admin.hipchat.com/admin
+1. Click on "Group Admin" -> "Integrations".
+1. Find "Build Your Own!" and click "Create".
+1. Select the desired room, name the integration "GitLab", and click "Create".
+1. In the "Send messages to this room by posting this URL" column, you should
+see a URL in the format:
+
+```
+ https://api.hipchat.com/v2/room/<room>/notification?auth_token=<token>
+```
+
+HipChat is now ready to accept messages from GitLab. Next, set up the HipChat
+service in GitLab.
+
+### Complete these steps in GitLab:
+
+1. Navigate to the project you want to configure for notifications.
+1. Select "Settings" in the top navigation.
+1. Select "Services" in the left navigation.
+1. Click "HipChat".
+1. Select the "Active" checkbox.
+1. Insert the `token` field from the URL into the `Token` field on the Web page.
+1. Insert the `room` field from the URL into the `Room` field on the Web page.
+1. Save or optionally click "Test Settings".
+
+## Troubleshooting
+
+If you do not see notifications, make sure you are using a HipChat v2 API
+token, not a v1 token.
+
+Note that the v2 token is tied to a specific room. If you want to be able to
+specify arbitrary rooms, you can create an API token for a specific user in
+HipChat under "Account settings" and "API access". Use the `XXX` value under
+`auth_token=XXX`.
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 86eda341d6c..03937d20728 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -12,7 +12,7 @@ __Project integrations with external services for continuous integration and mor
- Flowdock
- Gemnasium
- GitLab CI
-- HipChat
+- [HipChat](hipchat.md) An Atlassian product for private group chat and instant messaging.
- [Irker](irker.md) An IRC gateway to receive messages on repository updates.
- Pivotal Tracker
- Pushover
diff --git a/doc/public_access/public_access.md b/doc/public_access/public_access.md
index 4712c387021..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.
+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..bfef975024f 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:
```
@@ -155,6 +162,7 @@ Options:
```
BACKUP=timestamp_of_backup (required if more than one backup exists)
+force=yes (do not ask if the authorized_keys file should get regenerated)
```
Example output:
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 80b01ca4043..4fbd20762da 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -47,3 +47,12 @@ sudo gitlab-rake gitlab:import:all_users_to_all_groups
# installation from source
bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production
```
+
+## 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
+```
diff --git a/doc/release/master.md b/doc/release/master.md
index 19070b46a0d..9163e652003 100644
--- a/doc/release/master.md
+++ b/doc/release/master.md
@@ -31,3 +31,32 @@ git remote add gl git@gitlab.com:gitlab-org/gitlab-ce.git
gpa
```
+# Yanking packages from packages.gitlab.com
+
+In case something went wrong with the release and there is a need to remove the packages you can yank the packages by following the
+procedure described in [package cloud documentation](https://packagecloud.io/docs#yank_pkg).
+
+You need to have:
+
+1. `package_cloud` gem installed (sudo gem install package_cloud)
+1. Email and password for packages.gitlab.com
+1. Make sure that you are supplying the url to packages.gitlab.com (default is packagecloud.io)
+
+Example of yanking a package:
+
+```bash
+package_cloud yank --url https://packages.gitlab.com gitlab/gitlab-ce/el/6 gitlab-ce-7.10.2~omnibus-1.x86_64.rpm
+```
+
+If you are attempting this for the first time the output will look something like:
+
+```bash
+Looking for repository at gitlab/gitlab-ce... No config file exists at /Users/marin/.packagecloud. Login to create one.
+Email:
+marin@gitlab.com
+Password:
+
+Got your token. Writing a config file to /Users/marin/.packagecloud... success!
+success!
+Attempting to yank package at gitlab/gitlab-ce/el/6/gitlab-ce-7.10.2~omnibus-1.x86_64.rpm...done!
+```
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
index dd44c1eb860..cfe01896d8f 100644
--- a/doc/release/monthly.md
+++ b/doc/release/monthly.md
@@ -51,6 +51,7 @@ Xth: (5 working days before the 22nd)
Xth: (4 working days before the 22nd)
- [ ] Update GitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package)
+- [ ] Update ci.gitLab.com with rc1 (#LINK) (https://dev.gitlab.org/cookbooks/chef-repo/blob/master/doc/administration.md#deploy-the-package)
- [ ] Create regression issues (CE, CI) (#LINK)
- [ ] Tweet about rc1 (#LINK)
@@ -68,6 +69,7 @@ Xth: (1 working day before the 22nd)
- [ ] Create CE, EE, CI stable versions (#LINK)
- [ ] Create Omnibus tags and build packages
- [ ] Update GitLab.com with the stable version (#LINK)
+- [ ] Update ci.gitLab.com with the stable version (#LINK)
22nd:
@@ -176,6 +178,10 @@ Update [installation.md](/doc/install/installation.md) to the newest version in
Follow the [release doc in the Omnibus repository](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md).
This can happen before tagging because Omnibus uses tags in its own repo and SHA1's to refer to the GitLab codebase.
+## Update GitLab.com with the stable version
+
+- Deploy the package (should not need downtime because of the small difference with RC1)
+- Deploy the package for ci.gitlab.com
## Release CE, EE and CI
@@ -197,6 +203,10 @@ Proposed tweet "Release of GitLab X.X & CI Y.Y! FEATURE, FEATURE and FEATURE &lt
Consider creating a post on Hacker News.
-## Update GitLab.com with the stable version
+## Release new AMIs
-- Deploy the package (should not need downtime because of the small difference with RC1)
+[Follow this guide](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
+
+## Create a WIP blogpost for the next release
+
+Create a WIP blogpost using [release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/release_blog_template.md).
diff --git a/doc/release/patch.md b/doc/release/patch.md
index 80afa19b6c5..a569bb3da8d 100644
--- a/doc/release/patch.md
+++ b/doc/release/patch.md
@@ -35,22 +35,22 @@ 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
+## Release
1. [Build new packages with the latest version](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/release.md)
1. Apply the patch to GitLab.com and the private GitLab development server
+1. Apply the patch to ci.gitLab.com and the private GitLab CI development server
1. Create and publish a blog post, see [patch release blog template](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/doc/patch_release_blog_template.md)
1. Send tweets about the release from `@gitlab`, tweet should include the most important feature that the release is addressing and link to the blog post
1. Note in the 'GitLab X.X regressions' issue that the patch was published (CE only)
+1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
+1. Create a new patch release issue for the next potential release \ No newline at end of file
diff --git a/doc/release/security.md b/doc/release/security.md
index b67e0f37a04..60bcfbb6da5 100644
--- a/doc/release/security.md
+++ b/doc/release/security.md
@@ -18,11 +18,13 @@ Please report suspected security vulnerabilities in private to <support@gitlab.c
1. Do the steps from [patch release document](doc/release/patch.md), starting with "Create an issue on private GitLab development server"
1. The MR with the security fix should get a 'security' label and be assigned to the release manager
1. Build the package for GitLab.com and do a deploy
+1. Build the package for ci.gitLab.com and do a deploy
+1. [Create new AMIs](https://dev.gitlab.org/gitlab/AMI/blob/master/README.md)
1. Create feature branches for the blog post on GitLab.com and link them from the code branch
1. Merge and publish the blog posts
1. Send tweets about the release from `@gitlabhq`
1. Send out an email to [the community google mailing list](https://groups.google.com/forum/#!forum/gitlabhq)
-1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number
+1. Post a signed copy of our complete announcement to [oss-security](http://www.openwall.com/lists/oss-security/) and request a CVE number. CVE is only needed for bugs that allow someone to own the server (Remote Code Execution) or access to code of projects they are not a member of.
1. Add the security researcher to the [Security Researcher Acknowledgments list](http://about.gitlab.com/vulnerability-acknowledgements/)
1. Thank the security researcher in an email for their cooperation
1. Update the blog post and the CHANGELOG when we receive the CVE number
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 6fe23dfa2a6..0acf92fbf54 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -45,7 +45,7 @@ clip < ~/.ssh/id_rsa.pub
pbcopy < ~/.ssh/id_rsa.pub
```
-**Linux (requires xclip):**
+**GNU/Linux (requires xclip):**
```bash
xclip -sel clip < ~/.ssh/id_rsa.pub
```
@@ -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.8.md b/doc/update/6.x-or-7.x-to-7.10.md
index 673d9253d62..39e12f32d0e 100644
--- a/doc/update/6.x-or-7.x-to-7.8.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.8
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/6.x-or-7.x-to-7.8.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.8.
+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-8-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-8-stable-ee
+sudo -u git -H git checkout 7-10-stable-ee
```
## 4. Install additional packages
@@ -93,6 +93,9 @@ sudo apt-get install pkg-config cmake
# Install Kerberos header files, which are needed for GitLab EE Kerberos support
sudo apt-get install libkrb5-dev
+
+# Install nodejs, javascript runtime required for assets
+sudo apt-get install nodejs
```
## 5. Configure Redis to use sockets
@@ -123,7 +126,7 @@ sudo apt-get install libkrb5-dev
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
-sudo -u git -H git checkout v2.5.4
+sudo -u git -H git checkout v2.6.2
```
## 7. Install libs, migrations, etc.
@@ -158,12 +161,12 @@ 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-8-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-8-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-8-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.5.4/config.yml.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
```bash
@@ -178,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-8-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-8-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/update/7.6-to-7.7.md b/doc/update/7.6-to-7.7.md
index 59243713156..910c7dcdd3c 100644
--- a/doc/update/7.6-to-7.7.md
+++ b/doc/update/7.6-to-7.7.md
@@ -67,7 +67,7 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`](/config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
```
git diff origin/7-6-stable:config/gitlab.yml.example origin/7-7-stable:config/gitlab.yml.example
diff --git a/doc/update/7.8-to-7.9.md b/doc/update/7.8-to-7.9.md
new file mode 100644
index 00000000000..6ffa21c6141
--- /dev/null
+++ b/doc/update/7.8-to-7.9.md
@@ -0,0 +1,122 @@
+# From 7.8 to 7.9
+
+### 0. Stop server
+
+ sudo service gitlab stop
+
+### 1. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 2. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 7-9-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 7-9-stable-ee
+```
+
+### 3. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch
+sudo -u git -H git checkout v2.6.0
+```
+
+### 4. Install libs, migrations, etc.
+
+Please refer to the [Node.js setup documentation](https://github.com/joyent/node/wiki/installing-node.js-via-package-manager#debian-and-ubuntu-based-linux-distributions) if you aren't running default GitLab server setup.
+
+```bash
+sudo apt-get install nodejs
+
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without ... postgres')
+sudo -u git -H bundle install --without development test postgres --deployment
+
+# PostgreSQL installations (note: the line below states '--without ... mysql')
+sudo -u git -H bundle install --without development test mysql --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+# Update init.d script
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 5. Update config files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+
+```
+git diff origin/7-8-stable:config/gitlab.yml.example origin/7-9-stable:config/gitlab.yml.example
+```
+
+#### Change Nginx settings
+
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/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.
+
+#### Setup time zone (optional)
+
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+
+### 6. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 7. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check with:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations upgrade is complete!
+
+### 8. GitHub settings (if applicable)
+
+If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it
+only contains a root URL (ex. `https://gitlab.example.com/`)
+
+## Things went south? Revert to previous version (7.8)
+
+### 1. Revert the code to the previous version
+Follow the [upgrade guide from 7.7 to 7.8](7.7-to-7.8.md), except for the database migration
+(The backup is already migrated to the previous version)
+
+### 2. Restore from the backup:
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/7.9-to-7.10.md b/doc/update/7.9-to-7.10.md
new file mode 100644
index 00000000000..d1179dc2ec7
--- /dev/null
+++ b/doc/update/7.9-to-7.10.md
@@ -0,0 +1,118 @@
+# From 7.9 to 7.10
+
+### 0. Stop server
+
+ sudo service gitlab stop
+
+### 1. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 2. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 7-10-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 7-10-stable-ee
+```
+
+### 3. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch
+sudo -u git -H git checkout v2.6.2
+```
+
+### 4. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without ... postgres')
+sudo -u git -H bundle install --without development test postgres --deployment
+
+# PostgreSQL installations (note: the line below states '--without ... mysql')
+sudo -u git -H bundle install --without development test mysql --deployment
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+
+# Update init.d script
+sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+```
+
+### 5. Update config files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+
+```
+git diff origin/7-9-stable:config/gitlab.yml.example origin/7-10-stable:config/gitlab.yml.example
+```
+
+#### Change Nginx settings
+
+* HTTP setups: Make `/etc/nginx/sites-available/gitlab` the same as [`lib/support/nginx/gitlab`](/lib/support/nginx/gitlab) but with your settings.
+* HTTPS setups: Make `/etc/nginx/sites-available/gitlab-ssl` the same as [`lib/support/nginx/gitlab-ssl`](/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.
+
+#### Setup time zone (optional)
+
+Consider setting the time zone in `gitlab.yml` otherwise GitLab will default to UTC. If you set a time zone previously in [`application.rb`](config/application.rb) (unlikely), unset it.
+
+### 6. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 7. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check with:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations upgrade is complete!
+
+### 8. GitHub settings (if applicable)
+
+If you are using GitHub as an OAuth provider for authentication, you should change the callback URL so that it
+only contains a root URL (ex. `https://gitlab.example.com/`)
+
+## Things went south? Revert to previous version (7.9)
+
+### 1. Revert the code to the previous version
+Follow the [upgrade guide from 7.8 to 7.9](7.8-to-7.9.md), except for the database migration
+(The backup is already migrated to the previous version)
+
+### 2. Restore from the backup:
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index ad302492556..e29ee2a7b3d 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -1,5 +1,5 @@
# Universal update guide for patch versions
-*Make sure you view this [upgrade guide from the `master` branch](../../../master/doc/update/patch_versions.md) for the most up to date instructions.*
+*Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.*
For example from 6.2.0 to 6.2.1, also see the [semantic versioning specification](http://semver.org/).
diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md
index 4ed35b2b562..d23534d58b6 100644
--- a/doc/update/upgrader.md
+++ b/doc/update/upgrader.md
@@ -24,14 +24,13 @@ If you have local changes to your GitLab repository the script will stash them a
## 2. Run GitLab upgrade tool
-Note: GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX) while 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies)
+Note: GitLab 7.9 adds `nodejs` as a dependency. GitLab 7.6 adds `libkrb5-dev` as a dependency (installed by default on Ubuntu and OSX). GitLab 7.2 adds `pkg-config` and `cmake` as dependency. Please check the dependencies in the [installation guide.](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md#1-packages-dependencies)
- # Starting with GitLab version 7.0 upgrader script has been moved to bin directory
cd /home/git/gitlab
- if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb; else sudo -u git -H ruby script/upgrade.rb; fi
+ sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"v7.10.1"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute'
# to perform a non-interactive install (no user input required) you can add -y
- # if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi
+ # sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"v7.10.1"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y
## 3. Start application
@@ -66,11 +65,12 @@ Here is a one line command with step 1 to 5 for the next time you upgrade:
cd /home/git/gitlab; \
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production; \
sudo service gitlab stop; \
- if [ -f bin/upgrade.rb ]; then sudo -u git -H ruby bin/upgrade.rb -y; else sudo -u git -H ruby script/upgrade.rb -y; fi; \
+ sudo -u git -H ruby -Ilib -e 'require "gitlab/upgrader"' -e 'class Gitlab::Upgrader' -e 'def latest_version_raw' -e '"v7.10.1"' -e 'end' -e 'end' -e 'Gitlab::Upgrader.new.execute' -- -y; \
cd /home/git/gitlab-shell; \
sudo -u git -H git fetch; \
sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`; \
cd /home/git/gitlab; \
sudo service gitlab start; \
- sudo service nginx restart; sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
-```
+ sudo service nginx restart; \
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+``` \ No newline at end of file
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 3cccd84b063..851f50f5e9a 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -16,6 +16,7 @@ Triggered when you push to the repository except when pushing tags.
```json
{
+ "object_kind": "push",
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"ref": "refs/heads/master",
@@ -66,6 +67,7 @@ Triggered when you create (or delete) tags to the repository.
```json
{
+ "object_kind": "tag_push",
"ref": "refs/tags/v1.0.0",
"before": "0000000000000000000000000000000000000000",
"after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
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/doc/workflow/protected_branches.md b/doc/workflow/protected_branches.md
index 805f7f8d35c..0adf9f8e3e8 100644
--- a/doc/workflow/protected_branches.md
+++ b/doc/workflow/protected_branches.md
@@ -12,7 +12,7 @@ A protected branch does three simple things:
You can make any branch a protected branch. GitLab makes the master branch a protected branch by default.
-To protect a branch, user needs to have at least a Master permission level, see [permissions document](permissions/permissions.md).
+To protect a branch, user needs to have at least a Master permission level, see [permissions document](doc/permissions/permissions.md).
![protected branches page](protected_branches/protected_branches1.png)
@@ -28,6 +28,4 @@ For those workflows, you can allow everyone with write access to push to a prote
On already protected branches you can also allow developers to push to the repository by selecting the `Developers can push` check box.
-![Developers can push](protected_branches/protected_branches2.png)
-
-
+![Developers can push](protected_branches/protected_branches2.png) \ No newline at end of file
diff --git a/docker/README.md b/docker/README.md
index 58982a238a8..2e533ae9dd5 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,60 +1,118 @@
-What is GitLab?
-===============
+# GitLab Docker images
-GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster. A subscription gives you access to our support team and to GitLab Enterprise Edition that contains extra features aimed at larger organizations.
+## What is GitLab?
-<https://about.gitlab.com>
+GitLab offers git repository management, code reviews, issue tracking, activity feeds, wikis. It has LDAP/AD integration, handles 25,000 users on a single server but can also run on a highly available active/active cluster.
+Learn more on [https://about.gitlab.com](https://about.gitlab.com)
-![GitLab Logo](https://gitlab.com/uploads/appearance/logo/1/brand_logo-c37eb221b456bb4b472cc1084480991f.png)
+## After starting a container
+After starting a container you can go to [http://localhost:8080/](http://localhost:8080/) or [http://192.168.59.103:8080/](http://192.168.59.103:8080/) if you use boot2docker.
-How to use this image
-======================
+It might take a while before the docker container is responding to queries.
-At this moment GitLab doesn't have official Docker images.
-Build your own based on the Omnibus packages with the following command (it assumes you're in the GitLab repo root directory):
+You can check the status with something like `sudo docker logs -f 7c10172d7705`.
+
+You can login to the web interface with username `root` and password `5iveL!fe`.
+
+Next time, you can just use docker start and stop to run the container.
+
+## How to build the docker images
+
+This guide will also let you know how to build docker images yourself.
+Please run all the commands from the GitLab repo root directory.
+People using boot2docker should run all the commands without sudo.
+
+## Choosing between the single and the app and data images
+
+Normally docker uses a single image for one applications.
+But GitLab stores repositories and uploads in the filesystem.
+This means that upgrades of a single image are hard.
+That is why we recommend using separate app and data images.
+We'll first describe how to use a single image.
+After that we'll describe how to use the app and data images.
+
+## Single image
+
+Get a published image from Dockerhub:
```bash
-sudo docker build --tag gitlab_image docker/
+sudo docker pull sytse/gitlab-ce:7.10.1
```
-We assume using a data volume container, this will simplify migrations and backups.
-This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it.
+Run the image:
-The directories on data container are:
+```bash
+sudo docker run --detach --publish 8080:80 --publish 2222:22 sytse/gitlab-ce:7.10.1
+```
-- `/var/opt/gitlab` for application data
-- `/var/log/gitlab` for logs
-- `/etc/gitlab` for configuration
+After this you can login to the web interface as explained above in 'After starting a container'.
+
+Build the image:
+
+```bash
+sudo docker build --tag sytse/gitlab-ce:7.10.1 docker/single/
+```
+
+Publish the image to Dockerhub:
+
+```bash
+sudo docker push sytse/gitlab-ce
+```
-Create the data container with:
+Diagnosing commands:
```bash
-sudo docker run --name gitlab_data gitlab_image /bin/true
+sudo docker run -i -t sytse/gitlab-ce:7.10.1
+sudo docker run -ti -e TERM=linux --name gitlab-ce-troubleshoot --publish 8080:80 --publish 2222:22 sytse/gitlab-ce:7.10.1 bash /usr/local/bin/wrapper
```
-After creating this run GitLab:
+## App and data images
+
+### Get published images from Dockerhub
```bash
-sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab_image
+sudo docker pull sytse/gitlab-data
+sudo docker pull sytse/gitlab-app:7.10.1
```
-It might take a while before the docker container is responding to queries. You can follow the configuration process with `docker logs -f gitlab_app`.
+### Run the images
-You can then go to `http://localhost:8080/` (or `http://192.168.59.103:8080/` if you use boot2docker).
-You can login with username `root` and password `5iveL!fe`.
-Next time, you can just use `sudo docker start gitlab_app` and `sudo docker stop gitlab_app`.
+```bash
+sudo docker run --name gitlab-data sytse/gitlab-data /bin/true
+sudo docker run --detach --name gitlab_app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data sytse/gitlab-app:7.10.1
+```
+After this you can login to the web interface as explained above in 'After starting a container'.
-How to configure GitLab
-========================
+### Build images
-This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`.
+Build your own based on the Omnibus packages with the following commands.
+
+```bash
+sudo docker build --tag gitlab-data docker/data/
+sudo docker build --tag gitlab-app:7.10.1 docker/app/
+```
+
+After this run the images as described in the previous section.
+
+We assume using a data volume container, this will simplify migrations and backups.
+This empty container will exist to persist as volumes the 3 directories used by GitLab, so remember not to delete it.
+
+The directories on data container are:
+
+- `/var/opt/gitlab` for application data
+- `/var/log/gitlab` for logs
+- `/etc/gitlab` for configuration
+
+### Configure GitLab
+
+These container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`.
To access GitLab configuration, you can start an interactive command line in a new container using the shared data volume container, you will be able to browse the 3 directories and use your favorite text editor:
```bash
-docker run -ti -e TERM=linux --rm --volumes-from gitlab_data ubuntu
+sudo docker run -ti -e TERM=linux --rm --volumes-from gitlab-data ubuntu
vi /etc/gitlab/gitlab.rb
```
@@ -62,7 +120,36 @@ vi /etc/gitlab/gitlab.rb
You can find all available options in [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration).
+### Upgrade GitLab with app and data images
+
+To updgrade GitLab to new versions, stop running container, create new docker image and container from that image.
+
+It Assumes that you're upgrading from 7.8.1 to 7.10.1 and you're in the updated GitLab repo root directory:
+
+```bash
+sudo docker stop gitlab-app
+sudo docker rm gitlab-app
+sudo docker build --tag gitlab-app:7.10.1 docker/app/
+sudo docker run --detach --name gitlab-app --publish 8080:80 --publish 2222:22 --volumes-from gitlab_data gitlab-app:7.10.1
+```
+
+On the first run GitLab will reconfigure and update itself. If everything runs OK don't forget to cleanup the app image:
+
+```bash
+sudo docker rmi gitlab-app:7.8.1
+```
+
+### Publish images to Dockerhub
+
+Login to Dockerhub with `sudo docker login` and run the following (replace '7.9.2' with the version you're using and 'Sytse Sijbrandij' with your name):
+
+```bash
+sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab-app:7.10.1 sytse/gitlab-app:7.10.1
+sudo docker push sytse/gitlab-app:7.10.1
+sudo docker commit -m "Initial commit" -a "Sytse Sijbrandij" gitlab_data sytse/gitlab_data
+sudo docker push sytse/gitlab_data
+```
+
+## Troubleshooting
-Troubleshooting
-=========================
-Please see the [troubleshooting](troubleshooting.md) file in this directory.
+Please see the [troubleshooting](troubleshooting.md) file in this directory. \ No newline at end of file
diff --git a/docker/Dockerfile b/docker/app/Dockerfile
index 3584a754c62..df828a2a349 100644
--- a/docker/Dockerfile
+++ b/docker/app/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.8.1-omnibus-1_amd64.deb \
+ wget -q -O $TMP_FILE https://downloads-packages.s3.amazonaws.com/ubuntu-14.04/gitlab-ce_7.10.1~omnibus.2-1_amd64.deb \
&& dpkg -i $TMP_FILE \
&& rm -f $TMP_FILE
@@ -26,12 +26,8 @@ RUN mkdir -p /opt/gitlab/sv/sshd/supervise \
# Expose web & ssh
EXPOSE 80 22
-# Declare volumes
-VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"]
-
# Copy assets
-COPY assets/gitlab.rb /etc/gitlab/
COPY assets/wrapper /usr/local/bin/
# Wrapper to handle signal, trigger runit and reconfigure GitLab
-CMD ["/usr/local/bin/wrapper"]
+CMD ["/usr/local/bin/wrapper"] \ No newline at end of file
diff --git a/docker/assets/wrapper b/docker/app/assets/wrapper
index 9e6e7a05903..9e6e7a05903 100755
--- a/docker/assets/wrapper
+++ b/docker/app/assets/wrapper
diff --git a/docker/data/Dockerfile b/docker/data/Dockerfile
new file mode 100644
index 00000000000..ea0175c4aa2
--- /dev/null
+++ b/docker/data/Dockerfile
@@ -0,0 +1,8 @@
+FROM busybox
+
+# Declare volumes
+VOLUME ["/var/opt/gitlab", "/var/log/gitlab", "/etc/gitlab"]
+# Copy assets
+COPY assets/gitlab.rb /etc/gitlab/
+
+CMD /bin/sh
diff --git a/docker/assets/gitlab.rb b/docker/data/assets/gitlab.rb
index 7fddf309c01..7fddf309c01 100644
--- a/docker/assets/gitlab.rb
+++ b/docker/data/assets/gitlab.rb
diff --git a/docker/single/Dockerfile b/docker/single/Dockerfile
new file mode 100644
index 00000000000..8cdc24cf045
--- /dev/null
+++ b/docker/single/Dockerfile
@@ -0,0 +1,35 @@
+FROM ubuntu:14.04
+MAINTAINER Sytse Sijbrandij
+
+# Install required packages
+RUN apt-get update
+ENV DEBIAN_FRONTEND noninteractive
+RUN apt-get install -yq --no-install-recommends \
+ ca-certificates \
+ openssh-server \
+ wget
+
+# Download & Install GitLab
+# 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-ce_7.10.1~omnibus.2-1_amd64.deb \
+ && dpkg -i $TMP_FILE \
+ && rm -f $TMP_FILE
+
+# Manage SSHD through runit
+RUN mkdir -p /opt/gitlab/sv/sshd/supervise \
+ && mkfifo /opt/gitlab/sv/sshd/supervise/ok \
+ && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \
+ && chmod a+x /opt/gitlab/sv/sshd/run \
+ && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \
+ && mkdir -p /var/run/sshd
+
+# Expose web & ssh
+EXPOSE 80 22
+
+# Copy assets
+COPY assets/wrapper /usr/local/bin/
+
+# Wrapper to handle signal, trigger runit and reconfigure GitLab
+CMD ["/usr/local/bin/wrapper"]
diff --git a/docker/single/assets/wrapper b/docker/single/assets/wrapper
new file mode 100755
index 00000000000..966b2cab4a1
--- /dev/null
+++ b/docker/single/assets/wrapper
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+function sigterm_handler() {
+ echo "SIGTERM signal received, try to gracefully shutdown all services..."
+ gitlab-ctl stop
+}
+
+trap "sigterm_handler; exit" TERM
+
+function entrypoint() {
+ /opt/gitlab/embedded/bin/runsvdir-start &
+ gitlab-ctl reconfigure # will also start everything
+ gitlab-ctl tail # tail all logs
+}
+
+entrypoint
diff --git a/docker/single/marathon.json b/docker/single/marathon.json
new file mode 100644
index 00000000000..d23c2b84e0e
--- /dev/null
+++ b/docker/single/marathon.json
@@ -0,0 +1,14 @@
+{
+ "id": "/gitlab",
+ "ports": [0,0],
+ "cpus": 2,
+ "mem": 2048.0,
+ "disk": 10240.0,
+ "container": {
+ "type": "DOCKER",
+ "docker": {
+ "network": "HOST",
+ "image": "sytse/gitlab-ce:7.10.1"
+ }
+ }
+} \ No newline at end of file
diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md
index b1b70de5997..5827f2185db 100644
--- a/docker/troubleshooting.md
+++ b/docker/troubleshooting.md
@@ -61,3 +61,13 @@ head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall
free -m
```
+
+# Cleanup
+
+Remove ALL docker containers and images (also non GitLab ones):
+
+```
+docker rm $(docker ps -a -q)
+docker rmi $(docker images -q)
+```
+
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/admin/settings.feature b/features/admin/settings.feature
index 8fdf0575c2c..e38eea2cfed 100644
--- a/features/admin/settings.feature
+++ b/features/admin/settings.feature
@@ -7,3 +7,13 @@ Feature: Admin Settings
Scenario: Change application settings
When I modify settings and save form
Then I should see application settings saved
+
+ Scenario: Change Slack Service Template settings
+ When I click on "Service Templates"
+ And I click on "Slack" service
+ And I fill out Slack settings
+ Then I check all events and submit form
+ And I should see service template settings saved
+ Then I click on "Slack" service
+ And I should see all checkboxes checked
+ And I should see Slack settings saved
diff --git a/features/dashboard/archived_projects.feature b/features/dashboard/archived_projects.feature
index 3af93bc373c..69b3a776441 100644
--- a/features/dashboard/archived_projects.feature
+++ b/features/dashboard/archived_projects.feature
@@ -10,8 +10,3 @@ Feature: Dashboard Archived Projects
Scenario: I should see non-archived projects on dashboard
Then I should see "Shop" project link
And I should not see "Forum" project link
-
- Scenario: I should see all projects on projects page
- And I visit dashboard projects page
- Then I should see "Shop" project link
- And I should see "Forum" project link
diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature
index 92c1379ba77..cf4b8d7283b 100644
--- a/features/dashboard/group.feature
+++ b/features/dashboard/group.feature
@@ -46,3 +46,11 @@ Feature: Dashboard Group
When I visit dashboard groups page
Then I should see group "Owned" in group list
Then I should not see group "Guest" in group list
+
+ Scenario: Create a group from dasboard
+ And I visit dashboard groups page
+ And I click new group link
+ And submit form with new group "Samurai" info
+ Then I should be redirected to group "Samurai" page
+ And I should see newly created group "Samurai"
+
diff --git a/features/dashboard/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/dashboard/new_project.feature b/features/dashboard/new_project.feature
new file mode 100644
index 00000000000..431dc4ccfcb
--- /dev/null
+++ b/features/dashboard/new_project.feature
@@ -0,0 +1,13 @@
+@dashboard
+Feature: New Project
+Background:
+ Given I sign in as a user
+ And I own project "Shop"
+ And I visit dashboard page
+
+ @javascript
+ Scenario: I should see New projects page
+ Given I click "New project" link
+ Then I see "New project" page
+ When I click on "Import project from GitHub"
+ Then I see instructions on how to import from GitHub
diff --git a/features/dashboard/projects.feature b/features/dashboard/projects.feature
deleted file mode 100644
index bb4e84f0159..00000000000
--- a/features/dashboard/projects.feature
+++ /dev/null
@@ -1,9 +0,0 @@
-@dashboard
-Feature: Dashboard Projects
- Background:
- Given I sign in as a user
- And I own project "Shop"
- And I visit dashboard projects page
-
- Scenario: I should see projects list
- Then I should see projects list
diff --git a/features/groups.feature b/features/groups.feature
index b5ff03db844..415e43d6ae7 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -10,14 +10,6 @@ Feature: Groups
Then I should see group "Owned" projects list
And I should see projects activity feed
- Scenario: Create a group from dasboard
- When I visit group "Owned" page
- And I visit dashboard page
- And I click new group link
- And submit form with new group "Samurai" info
- Then I should be redirected to group "Samurai" page
- And I should see newly created group "Samurai"
-
Scenario: I should see group "Owned" issues list
Given project from group "Owned" has issues assigned to me
When I visit group "Owned" issues page
@@ -55,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/archived.feature b/features/project/archived.feature
index 9aac29384ba..ad466f4f307 100644
--- a/features/project/archived.feature
+++ b/features/project/archived.feature
@@ -14,15 +14,6 @@ Feature: Project Archived
And I visit project "Forum" page
Then I should see "Archived"
- Scenario: I should not see archived on projects page with no archived projects
- And I visit dashboard projects page
- Then I should not see "Archived"
-
- Scenario: I should see archived on projects page with archived projects
- And project "Forum" is archived
- And I visit dashboard projects page
- Then I should see "Archived"
-
Scenario: I archive project
When project "Shop" has push event
And I visit project "Shop" page
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 46076b6f3e6..c4b206edc95 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -21,10 +21,13 @@ Feature: Project Commits
And I click side-by-side diff button
Then I see inline diff button
+ @javascript
Scenario: I compare refs
Given I visit compare refs page
And I fill compare fields with refs
Then I see compared refs
+ And I unfold diff
+ Then I should see additional file lines
Scenario: I browse commits for a specific path
Given I visit my project's commits page for a specific path
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 283979204db..bf84e2f8e87 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
@@ -127,48 +134,15 @@ Feature: Project Issues
And I should see "Release 0.4" in issues
And I should not see "Tweet control" in issues
- Scenario: Issue description should render task checkboxes
- Given project "Shop" has "Tasks-open" open issue with task markdown
- When I visit issue page "Tasks-open"
- Then I should see task checkboxes in the description
-
- @javascript
- Scenario: Issue notes should not render task checkboxes
- Given project "Shop" has "Tasks-open" open issue with task markdown
- When I visit issue page "Tasks-open"
- And I leave a comment with task markdown
- Then I should not see task checkboxes in the comment
-
@javascript
Scenario: Issue notes should be editable with +1
- Given project "Shop" has "Tasks-open" open issue with task markdown
- When I visit issue page "Tasks-open"
+ Given project "Shop" have "Release 0.4" open issue
+ When I visit issue page "Release 0.4"
And I leave a comment with a header containing "Comment with a header"
Then The comment with the header should not have an ID
And I edit the last comment with a +1
Then I should see +1 in the description
- # Task status in issues list
-
- Scenario: Issues list should display task status
- Given project "Shop" has "Tasks-open" open issue with task markdown
- When I visit project "Shop" issues page
- Then I should see the task status for the Taskable
-
- # Toggling task items
-
- @javascript
- Scenario: Task checkboxes should be enabled for an open issue
- Given project "Shop" has "Tasks-open" open issue with task markdown
- When I visit issue page "Tasks-open"
- Then Task checkboxes should be enabled
-
- @javascript
- Scenario: Task checkboxes should be disabled for a closed issue
- Given project "Shop" has "Tasks-closed" closed issue with task markdown
- When I visit issue page "Tasks-closed"
- Then Task checkboxes should be disabled
-
# Issue description preview
@javascript
@@ -202,3 +176,11 @@ Feature: Project Issues
And I click link "Edit" for the issue
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab
+
+ @javascript
+ Scenario: I can unsubscribe from issue
+ Given project "Shop" have "Release 0.4" open issue
+ When I visit issue page "Release 0.4"
+ Then I should see that I am subscribed
+ When I click button "Unsubscribe"
+ Then I should see that I am unsubscribed
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 7c029f05d75..60caf783fe4 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -96,16 +96,6 @@ Feature: Project Merge Requests
And I leave a comment with a header containing "Comment with a header"
Then The comment with the header should not have an ID
- Scenario: Merge request description should render task checkboxes
- Given project "Shop" has "MR-task-open" open MR with task markdown
- When I visit merge request page "MR-task-open"
- Then I should see task checkboxes in the description
-
- Scenario: Merge request notes should not render task checkboxes
- Given project "Shop" has "MR-task-open" open MR with task markdown
- When I visit merge request page "MR-task-open"
- Then I should not see task checkboxes in the comment
-
# Toggling inline comments
@javascript
@@ -166,27 +156,12 @@ Feature: Project Merge Requests
And I click Side-by-side Diff tab
Then I should see comments on the side-by-side diff page
- # Task status in issues list
-
- Scenario: Merge requests list should display task status
- Given project "Shop" has "MR-task-open" open MR with task markdown
- When I visit project "Shop" merge requests page
- Then I should see the task status for the Taskable
-
- # Toggling task items
-
@javascript
- Scenario: Task checkboxes should be enabled for an open merge request
- Given project "Shop" has "MR-task-open" open MR with task markdown
- When I visit merge request page "MR-task-open"
- Then Task checkboxes should be enabled
-
- @javascript
- Scenario: Task checkboxes should be disabled for a closed merge request
- Given project "Shop" has "MR-task-open" open MR with task markdown
- And I visit merge request page "MR-task-open"
- And I click link "Close"
- Then Task checkboxes should be disabled
+ Scenario: I view diffs on a merge request
+ Given project "Shop" have "Bug NS-05" open merge request with diffs inside
+ And I visit merge request page "Bug NS-05"
+ And I click on the Changes tab via Javascript
+ Then I should see the proper Inline and Side-by-side links
# Description preview
@@ -218,3 +193,17 @@ Feature: Project Merge Requests
And I click link "Edit" for the merge request
And I preview a description text like "Bug fixed :smile:"
Then I should see the Markdown write tab
+
+ @javascript
+ Scenario: I search merge request
+ Given I click link "All"
+ When I fill in merge request search with "Fe"
+ Then I should see "Feature NS-03" in merge requests
+ And I should not see "Bug NS-04" in merge requests
+
+ @javascript
+ Scenario: I can unsubscribe from merge request
+ Given I visit merge request page "Bug NS-04"
+ Then I should see that I am subscribed
+ When I click button "Unsubscribe"
+ Then I should see that I am unsubscribed
diff --git a/features/project/project.feature b/features/project/project.feature
index 3e1fd54bee8..ae28312a69a 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -55,3 +55,10 @@ Feature: Project
Then I should see project "Forum" README
And I visit project "Shop" page
Then I should see project "Shop" README
+
+ Scenario: I tag a project
+ When I visit edit project "Shop" page
+ Then I should see project settings
+ And I add project tags
+ And I save project
+ Then I should see project tags
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 86ea6cd6e91..09a7df59df6 100644
--- a/features/project/team_management.feature
+++ b/features/project/team_management.feature
@@ -3,30 +3,36 @@ Feature: Project Team Management
Given I sign in as a user
And I own project "Shop"
And gitlab user "Mike"
- And gitlab user "Sam"
- And "Sam" is "Shop" developer
+ And gitlab user "Dmitriy"
+ And "Dmitriy" is "Shop" developer
And I visit project "Shop" team page
Scenario: See all team members
Then I should be able to see myself in team
- And I should see "Sam" in team list
+ And I should see "Dmitriy" in team list
@javascript
Scenario: Add user to project
- Given I click link "New Team Member"
+ Given I click link "Add members"
And I select "Mike" as "Reporter"
Then I should see "Mike" in team list as "Reporter"
@javascript
+ Scenario: Invite user to project
+ Given I click link "Add members"
+ And I select "sjobs@apple.com" as "Reporter"
+ 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"
- And I should see "Sam" in team list as "Reporter"
+ Given I should see "Dmitriy" in team list as "Developer"
+ And I change "Dmitriy" role to "Reporter"
+ And I should see "Dmitriy" in team list as "Reporter"
Scenario: Cancel team member
- Given I click cancel link for "Sam"
+ Given I click cancel link for "Dmitriy"
Then I visit project "Shop" team page
- And I should not see "Sam" in team list
+ And I should not see "Dmitriy" in team list
Scenario: Import team from another project
Given I own project "Website"
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/search.feature b/features/search.feature
index def21e00923..1608e824671 100644
--- a/features/search.feature
+++ b/features/search.feature
@@ -44,3 +44,9 @@ Feature: Search
Then I should see "Foo" link in the search results
And I should not see "Bar" link in the search results
+ Scenario: I should see Wiki blobs
+ And project has Wiki content
+ When I click project "Shop" link
+ And I search for "Wiki content"
+ And I click "Wiki" link
+ Then I should see "test_wiki" link in the search results
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/admin/groups.rb b/features/steps/admin/groups.rb
index 6bcec48be88..721460b9371 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -38,7 +38,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
When 'I select user "John Doe" from user list as "Reporter"' do
select2(user_john.id, from: "#user_ids", multiple: true)
- within "#new_team_member" do
+ within "#new_project_member" do
select "Reporter", from: "access_level"
end
click_button "Add users to group"
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index c2d0d2a3fa3..15ca0d80f1a 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -15,4 +15,44 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
current_application_settings.home_page_url.should == 'https://about.gitlab.com/'
page.should have_content 'Application settings saved successfully'
end
+
+ step 'I click on "Service Templates"' do
+ click_link 'Service Templates'
+ end
+
+ step 'I click on "Slack" service' do
+ click_link 'Slack'
+ end
+
+ step 'I check all events and submit form' do
+ page.check('Active')
+ page.check('Push events')
+ page.check('Tag push events')
+ page.check('Comments')
+ page.check('Issues events')
+ page.check('Merge Request events')
+ click_on 'Save'
+ end
+
+ step 'I fill out Slack settings' do
+ fill_in 'Webhook', with: 'http://localhost'
+ fill_in 'Username', with: 'test_user'
+ fill_in 'Channel', with: '#test_channel'
+ end
+
+ step 'I should see service template settings saved' do
+ page.should have_content 'Application settings saved successfully'
+ end
+
+ step 'I should see all checkboxes checked' do
+ all('input[type=checkbox]').each do |checkbox|
+ checkbox.should be_checked
+ end
+ end
+
+ step 'I should see Slack settings saved' do
+ find_field('Webhook').value.should eq 'http://localhost'
+ find_field('Username').value.should eq 'test_user'
+ find_field('Channel').value.should eq '#test_channel'
+ end
end
diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb
index 09d7717b67b..8384df2fb59 100644
--- a/features/steps/dashboard/group.rb
+++ b/features/steps/dashboard/group.rb
@@ -41,4 +41,23 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
step 'I should not see group "Guest" in group list' do
page.should_not have_content("Guest")
end
+
+ step 'I click new group link' do
+ click_link "New Group"
+ end
+
+ step 'submit form with new group "Samurai" info' do
+ fill_in 'group_path', with: 'Samurai'
+ fill_in 'group_description', with: 'Tokugawa Shogunate'
+ click_button "Create group"
+ end
+
+ step 'I should be redirected to group "Samurai" page' do
+ current_path.should == group_path(Group.find_by(name: 'Samurai'))
+ end
+
+ step 'I should see newly created group "Samurai"' do
+ page.should have_content "Samurai"
+ page.should have_content "Tokugawa Shogunate"
+ 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/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
new file mode 100644
index 00000000000..93456a81ecf
--- /dev/null
+++ b/features/steps/dashboard/new_project.rb
@@ -0,0 +1,29 @@
+class Spinach::Features::NewProject < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedProject
+
+ step 'I click "New project" link' do
+ within('.content') do
+ click_link "New project"
+ end
+ end
+
+ step 'I see "New project" page' do
+ page.should have_content("Project path")
+ end
+
+ step 'I click on "Import project from GitHub"' do
+ first('.how_to_import_link').click
+ end
+
+ step 'I see instructions on how to import from GitHub' do
+ github_modal = first('.modal-body')
+ github_modal.should be_visible
+ github_modal.should have_content "To enable importing projects from GitHub"
+
+ all('.modal-body').each do |element|
+ element.should_not be_visible unless element == github_modal
+ end
+ end
+end
diff --git a/features/steps/dashboard/projects.rb b/features/steps/dashboard/projects.rb
deleted file mode 100644
index 2a348163060..00000000000
--- a/features/steps/dashboard/projects.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class Spinach::Features::DashboardProjects < Spinach::FeatureSteps
- include SharedAuthentication
- include SharedPaths
- include SharedProject
-
- step 'I should see projects list' do
- @user.authorized_projects.all.each do |project|
- page.should have_link project.name_with_namespace
- end
- end
-end
diff --git a/features/steps/explore/groups.rb b/features/steps/explore/groups.rb
index ccbf6cda07e..0c2127d4c4b 100644
--- a/features/steps/explore/groups.rb
+++ b/features/steps/explore/groups.rb
@@ -35,7 +35,7 @@ class Spinach::Features::ExploreGroups < Spinach::FeatureSteps
end
step 'I visit group "TestGroup" members page' do
- visit members_group_path(Group.find_by(name: "TestGroup"))
+ visit group_group_members_path(Group.find_by(name: "TestGroup"))
end
step 'I should not see project "Enterprise" items' do
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index c3c34070e2e..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
@@ -72,25 +115,6 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
author: current_user
end
- When 'I click new group link' do
- click_link "New group"
- end
-
- step 'submit form with new group "Samurai" info' do
- fill_in 'group_path', with: 'Samurai'
- fill_in 'group_description', with: 'Tokugawa Shogunate'
- click_button "Create group"
- end
-
- step 'I should be redirected to group "Samurai" page' do
- current_path.should == group_path(Group.find_by(name: 'Samurai'))
- end
-
- step 'I should see newly created group "Samurai"' do
- page.should have_content "Samurai"
- page.should have_content "Tokugawa Shogunate"
- end
-
step 'I change group "Owned" name to "new-name"' do
fill_in 'group_name', with: 'new-name'
fill_in 'group_path', with: 'new-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/profile/profile.rb b/features/steps/profile/profile.rb
index bfbfe7af199..791982d16c3 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -11,6 +11,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in "user_linkedin", with: "testlinkedin"
fill_in "user_twitter", with: "testtwitter"
fill_in "user_website_url", with: "testurl"
+ fill_in "user_location", with: "Ukraine"
click_button "Save changes"
@user.reload
end
@@ -20,6 +21,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
@user.linkedin.should == 'testlinkedin'
@user.twitter.should == 'testtwitter'
@user.website_url.should == 'testurl'
+ find("#user_location").value.should == "Ukraine"
end
step 'I change my avatar' do
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index b2dccf868b0..30b1934b363 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -18,7 +18,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'I see commits atom feed' do
commit = @project.repository.commit
response_headers['Content-Type'].should have_content("application/atom+xml")
- body.should have_selector("title", text: "Recent commits to #{@project.name}")
+ body.should have_selector("title", text: "#{@project.name}:master commits")
body.should have_selector("author email", text: commit.author_email)
body.should have_selector("entry summary", text: commit.description[0..10])
end
@@ -38,6 +38,18 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
click_button "Compare"
end
+ step 'I unfold diff' do
+ @diff = first('.js-unfold')
+ @diff.click
+ sleep 2
+ end
+
+ step 'I should see additional file lines' do
+ within @diff.parent do
+ first('.new_line').text.should_not have_content "..."
+ end
+ end
+
step 'I see compared refs' do
page.should have_content "Compare View"
page.should have_content "Commits (1)"
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/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 63ad90e1241..94d21d28a0c 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -46,7 +46,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I submit the merge request' do
- click_button "Submit merge request"
+ click_button "Submit new merge request"
end
step 'I follow the target commit link' do
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 6d72c93ad13..504f0cff724 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -18,10 +18,23 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
page.should_not have_content "Tweet control"
end
+ step 'I should see that I am subscribed' do
+ find(".subscribe-button span").text.should == "Unsubscribe"
+ end
+
+ step 'I should see that I am unsubscribed' do
+ sleep 0.2
+ find(".subscribe-button span").text.should == "Subscribe"
+ end
+
step 'I click link "Closed"' do
click_link "Closed"
end
+ step 'I click button "Unsubscribe"' do
+ click_on "Unsubscribe"
+ end
+
step 'I should see "Release 0.3" in issues' do
page.should have_content "Release 0.3"
end
@@ -46,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"
@@ -154,14 +179,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
author: project.users.first)
end
- step 'project "Shop" has "Tasks-open" open issue with task markdown' do
- create_taskable(:issue, 'Tasks-open')
- end
-
- step 'project "Shop" has "Tasks-closed" closed issue with task markdown' do
- create_taskable(:closed_issue, 'Tasks-closed')
- end
-
step 'empty project "Empty Project"' do
create :empty_project, name: 'Empty Project', namespace: @user.namespace
end
@@ -191,6 +208,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 263f2ef2438..f67e6e3d8ca 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -56,6 +56,18 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
page.should_not have_content "Bug NS-04"
end
+ step 'I should see that I am subscribed' do
+ find(".subscribe-button span").text.should == "Unsubscribe"
+ end
+
+ step 'I should see that I am unsubscribed' do
+ find(".subscribe-button span").should have_content("Subscribe")
+ end
+
+ step 'I click button "Unsubscribe"' do
+ click_on "Unsubscribe"
+ end
+
step 'I click link "Close"' do
first(:css, '.close-mr-link').click
end
@@ -65,7 +77,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
select "feature", from: "merge_request_target_branch"
click_button "Compare branches"
fill_in "merge_request_title", with: "Wiki Feature"
- click_button "Submit merge request"
+ click_button "Submit new merge request"
end
step 'project "Shop" have "Bug NS-04" open merge request' do
@@ -96,14 +108,24 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
author: project.users.first)
end
- step 'project "Shop" has "MR-task-open" open MR with task markdown' do
- create_taskable(:merge_request, 'MR-task-open')
- end
-
step 'I switch to the diff tab' do
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
+ step 'I click on the Changes tab via Javascript' do
+ find('.diffs-tab').click
+ sleep 2
+ end
+
+ step 'I should see the proper Inline and Side-by-side links' do
+ buttons = all('#commit-diff-viewtype')
+ expect(buttons.count).to eq(2)
+
+ buttons.each do |b|
+ expect(b['href']).should_not have_content('json')
+ end
+ end
+
step 'I switch to the merge request\'s comments tab' do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
@@ -196,13 +218,13 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
step 'I click link "Hide inline discussion" of the second file' do
within '.files [id^=diff]:nth-child(2)' do
- click_link 'Show/Hide comments'
+ find('.js-toggle-diff-comments').click
end
end
step 'I click link "Show inline discussion" of the second file' do
within '.files [id^=diff]:nth-child(2)' do
- click_link 'Show/Hide comments'
+ find('.js-toggle-diff-comments').click
end
end
@@ -276,6 +298,10 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
+ step 'I fill in merge request search with "Fe"' do
+ fill_in 'issue_search', with: "Fe"
+ end
+
def merge_request
@merge_request ||= MergeRequest.find_by!(title: "Bug NS-05")
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index d39c8e7d2db..00706ab30e9 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -68,7 +68,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'I should see project "Shop" version' do
within '.project-side' do
- page.should have_content 'Version: 6.7.0.pre'
+ page.should have_content '6.7.0.pre'
end
end
@@ -94,4 +94,12 @@ class Spinach::Features::Project < Spinach::FeatureSteps
page.should have_link 'README.md'
page.should have_content 'testme'
end
+
+ step 'I add project tags' do
+ fill_in 'Tags', with: 'tag1, tag2'
+ end
+
+ step 'I should see project tags' do
+ expect(find_field('Tags').value).to eq 'tag1, tag2'
+ end
end
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/source/search_code.rb b/features/steps/project/source/search_code.rb
index 9c2864cc936..b66c5a4123a 100644
--- a/features/steps/project/source/search_code.rb
+++ b/features/steps/project/source/search_code.rb
@@ -14,6 +14,6 @@ class Spinach::Features::ProjectSourceSearchCode < Spinach::FeatureSteps
end
step 'I should see empty result' do
- page.should have_content "We couldn't find any matching"
+ page.should have_content "We couldn't find any"
end
end
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 7907f2a6fe3..09e5af3ef48 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -9,24 +9,24 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
page.should have_content(@user.username)
end
- step 'I should see "Sam" in team list' do
- user = User.find_by(name: "Sam")
+ step 'I should see "Dmitriy" in team list' do
+ user = User.find_by(name: "Dmitriy")
page.should have_content(user.name)
page.should have_content(user.username)
end
- step 'I click link "New Team Member"' do
- click_link "New project member"
+ 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")
- select2(user.id, from: "#user_ids", multiple: true)
- within "#new_project_member" do
+ within ".users-project-form" do
+ select2(user.id, from: "#user_ids", multiple: true)
select "Reporter", from: "access_level"
end
- click_button "Add users"
+ click_button "Add users to project"
end
step 'I should see "Mike" in team list as "Reporter"' do
@@ -35,22 +35,42 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
end
end
- step 'I should see "Sam" in team list as "Developer"' do
+ 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 "Dmitriy" in team list as "Developer"' do
within ".access-developer" do
- page.should have_content('Sam')
+ page.should have_content('Dmitriy')
end
end
- step 'I change "Sam" role to "Reporter"' do
- user = User.find_by(name: "Sam")
- within "#user_#{user.id}" do
+ step 'I change "Dmitriy" role to "Reporter"' do
+ project = Project.find_by(name: "Shop")
+ user = User.find_by(name: 'Dmitriy')
+ project_member = project.project_members.find_by(user_id: user.id)
+ within "#project_member_#{project_member.id}" do
+ click_button "Edit access level"
select "Reporter", from: "project_member_access_level"
+ click_button "Save"
end
end
- step 'I should see "Sam" in team list as "Reporter"' do
+ step 'I should see "Dmitriy" in team list as "Reporter"' do
within ".access-reporter" do
- page.should have_content('Sam')
+ page.should have_content('Dmitriy')
end
end
@@ -58,8 +78,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_link "Remove from team"
end
- step 'I should not see "Sam" in team list' do
- user = User.find_by(name: "Sam")
+ step 'I should not see "Dmitriy" in team list' do
+ user = User.find_by(name: "Dmitriy")
page.should_not have_content(user.name)
page.should_not have_content(user.username)
end
@@ -68,12 +88,12 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
create(:user, name: "Mike")
end
- step 'gitlab user "Sam"' do
- create(:user, name: "Sam")
+ step 'gitlab user "Dmitriy"' do
+ create(:user, name: "Dmitriy")
end
- step '"Sam" is "Shop" developer' do
- user = User.find_by(name: "Sam")
+ step '"Dmitriy" is "Shop" developer' do
+ user = User.find_by(name: "Dmitriy")
project = Project.find_by(name: "Shop")
project.team << [user, :developer]
end
@@ -99,8 +119,11 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
click_button 'Import'
end
- step 'I click cancel link for "Sam"' do
- within "#user_#{User.find_by(name: 'Sam').id}" do
+ step 'I click cancel link for "Dmitriy"' do
+ project = Project.find_by(name: "Shop")
+ user = User.find_by(name: 'Dmitriy')
+ project_member = project.project_members.find_by(user_id: user.id)
+ within "#project_member_#{project_member.id}" do
click_link('Remove user from team')
end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index cd7d5eac243..717132da45d 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,46 @@ 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
+
+ step 'I search for Wiki content' do
+ fill_in "Search in this project", with: "wiki_content"
+ click_button "Search"
+ end
+
def wiki
@project_wiki = ProjectWiki.new(project, current_user)
end
diff --git a/features/steps/search.rb b/features/steps/search.rb
index 6f0e038c4d6..8197cd410aa 100644
--- a/features/steps/search.rb
+++ b/features/steps/search.rb
@@ -18,6 +18,11 @@ class Spinach::Features::Search < Spinach::FeatureSteps
click_button "Search"
end
+ step 'I search for "Wiki content"' do
+ fill_in "dashboard_search", with: "content"
+ click_button "Search"
+ end
+
step 'I click "Issues" link' do
within '.search-filter' do
click_link 'Issues'
@@ -36,6 +41,12 @@ class Spinach::Features::Search < Spinach::FeatureSteps
end
end
+ step 'I click "Wiki" link' do
+ within '.search-filter' do
+ click_link 'Wiki'
+ end
+ end
+
step 'I should see "Shop" project link' do
page.should have_link "Shop"
end
@@ -66,4 +77,13 @@ class Spinach::Features::Search < Spinach::FeatureSteps
step 'I should not see "Bar" link in the search results' do
find(:css, '.search-results').should_not have_link 'Bar'
end
+
+ step 'I should see "test_wiki" link in the search results' do
+ find(:css, '.search-results').should have_link 'test_wiki.md'
+ end
+
+ step 'project has Wiki content' do
+ @wiki = ::ProjectWiki.new(project, current_user)
+ @wiki.create_page("test_wiki", "Some Wiki content", :markdown, "first commit")
+ end
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index c229864bc83..9beb688bd16 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -26,7 +26,7 @@ module SharedActiveTab
end
step 'the active main tab should be Home' do
- ensure_active_main_tab('Activity')
+ ensure_active_main_tab('Your Projects')
end
step 'the active main tab should be Projects' do
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index e71700880cd..943640007a9 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -2,59 +2,18 @@ module SharedMarkdown
include Spinach::DSL
def header_should_have_correct_id_and_link(level, text, id, parent = ".wiki")
- find(:css, "#{parent} h#{level}##{id}").text.should == text
- find(:css, "#{parent} h#{level}##{id} > :last-child")[:href].should =~ /##{id}$/
- end
-
- def create_taskable(type, title)
- desc_text = <<EOT.gsub(/^ {6}/, '')
- * [ ] Task 1
- * [x] Task 2
-EOT
-
- case type
- when :issue, :closed_issue
- options = { project: project }
- when :merge_request
- options = { source_project: project, target_project: project }
- end
+ node = find("#{parent} h#{level} a##{id}")
+ node[:href].should == "##{id}"
- create(
- type,
- options.merge(title: title,
- author: project.users.first,
- description: desc_text)
- )
+ # Work around a weird Capybara behavior where calling `parent` on a node
+ # returns the whole document, not the node's actual parent element
+ find(:xpath, "#{node.path}/..").text.should == text
end
step 'Header "Description header" should have correct id and link' do
header_should_have_correct_id_and_link(1, 'Description header', 'description-header')
end
- step 'I should see task checkboxes in the description' do
- expect(page).to have_selector(
- 'div.description li.task-list-item input[type="checkbox"]'
- )
- end
-
- step 'I should see the task status for the Taskable' do
- expect(find(:css, 'span.task-status').text).to eq(
- '2 tasks (1 done, 1 unfinished)'
- )
- end
-
- step 'Task checkboxes should be enabled' do
- expect(page).to have_selector(
- 'div.description li.task-list-item input[type="checkbox"]:enabled'
- )
- end
-
- step 'Task checkboxes should be disabled' do
- expect(page).to have_selector(
- 'div.description li.task-list-item input[type="checkbox"]:disabled'
- )
- end
-
step 'I should not see the Markdown preview' do
expect(find('.gfm-form .js-md-preview')).not_to be_visible
end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 583746d4475..2f66e61b214 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -122,20 +122,6 @@ module SharedNote
end
end
- step 'I leave a comment with task markdown' do
- within('.js-main-target-form') do
- fill_in 'note[note]', with: '* [x] Task item'
- click_button 'Add Comment'
- sleep 0.05
- end
- end
-
- step 'I should not see task checkboxes in the comment' do
- expect(page).not_to have_selector(
- 'li.note div.timeline-content input[type="checkbox"]'
- )
- end
-
step 'I edit the last comment with a +1' do
find(".note").hover
find('.js-note-edit').click
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index bb6c336d7cd..d9401bd540c 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -32,7 +32,7 @@ module SharedPaths
end
step 'I visit group "Owned" members page' do
- visit members_group_path(Group.find_by(name:"Owned"))
+ visit group_group_members_path(Group.find_by(name:"Owned"))
end
step 'I visit group "Owned" settings page' do
@@ -52,7 +52,7 @@ module SharedPaths
end
step 'I visit group "Guest" members page' do
- visit members_group_path(Group.find_by(name:"Guest"))
+ visit group_group_members_path(Group.find_by(name:"Guest"))
end
step 'I visit group "Guest" settings page' do
@@ -323,16 +323,6 @@ module SharedPaths
visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
end
- step 'I visit issue page "Tasks-open"' do
- issue = Issue.find_by(title: 'Tasks-open')
- visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
- end
-
- step 'I visit issue page "Tasks-closed"' do
- issue = Issue.find_by(title: 'Tasks-closed')
- visit namespace_project_issue_path(issue.project.namespace, issue.project, issue)
- end
-
step 'I visit project "Shop" labels page' do
project = Project.find_by(name: 'Shop')
visit namespace_project_labels_path(project.namespace, project)
@@ -363,16 +353,6 @@ module SharedPaths
visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
end
- step 'I visit merge request page "MR-task-open"' do
- mr = MergeRequest.find_by(title: 'MR-task-open')
- visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
- end
-
- step 'I visit merge request page "MR-task-closed"' do
- mr = MergeRequest.find_by(title: 'MR-task-closed')
- visit namespace_project_merge_request_path(mr.target_project.namespace, mr.target_project, mr)
- end
-
step 'I visit project "Shop" merge requests page' do
visit namespace_project_merge_requests_path(project.namespace, project)
end
@@ -386,7 +366,7 @@ module SharedPaths
end
step 'I visit project "Shop" team page' do
- visit namespace_project_team_index_path(project.namespace, project)
+ visit namespace_project_project_members_path(project.namespace, project)
end
step 'I visit project wiki page' do
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/features/steps/snippets/user.rb b/features/steps/snippets/user.rb
index 866f637ab6c..146cc535d88 100644
--- a/features/steps/snippets/user.rb
+++ b/features/steps/snippets/user.rb
@@ -32,19 +32,19 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
end
step 'I click "Internal" filter' do
- within('.nav-stacked') do
+ within('.nav-tabs') do
click_link "Internal"
end
end
step 'I click "Private" filter' do
- within('.nav-stacked') do
+ within('.nav-tabs') do
click_link "Private"
end
end
step 'I click "Public" filter' do
- within('.nav-stacked') do
+ within('.nav-tabs') do
click_link "Public"
end
end
diff --git a/features/steps/user.rb b/features/steps/user.rb
index d6f05ecb2c7..10cae692a88 100644
--- a/features/steps/user.rb
+++ b/features/steps/user.rb
@@ -7,4 +7,37 @@ class Spinach::Features::User < Spinach::FeatureSteps
step 'I should see user "John Doe" page' do
expect(title).to match(/^\s*John Doe/)
end
+
+ step '"John Doe" has contributions' do
+ user = User.find_by(name: 'John Doe')
+ project = contributed_project
+
+ # Issue controbution
+ issue_params = { title: 'Bug in old browser' }
+ Issues::CreateService.new(project, user, issue_params).execute
+
+ # Push code contribution
+ push_params = {
+ project: project,
+ action: Event::PUSHED,
+ author_id: user.id,
+ data: { commit_count: 3 }
+ }
+
+ Event.create(push_params)
+ end
+
+ step 'I should see contributed projects' do
+ within '.contributed-projects' do
+ page.should have_content(@contributed_project.name)
+ end
+ end
+
+ step 'I should see contributions calendar' do
+ page.should have_css('.cal-heatmap-container')
+ end
+
+ def contributed_project
+ @contributed_project ||= create(:project, :public)
+ end
end
diff --git a/features/support/capybara.rb b/features/support/capybara.rb
new file mode 100644
index 00000000000..31dbf0feb2f
--- /dev/null
+++ b/features/support/capybara.rb
@@ -0,0 +1,24 @@
+require 'spinach/capybara'
+require 'capybara/poltergeist'
+
+# Give CI some extra time
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
+
+Capybara.javascript_driver = :poltergeist
+Capybara.register_driver :poltergeist do |app|
+ Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
+end
+
+Spinach.hooks.on_tag("javascript") do
+ Capybara.current_driver = Capybara.javascript_driver
+end
+
+Capybara.default_wait_time = timeout
+Capybara.ignore_hidden_elements = false
+
+unless ENV['CI'] || ENV['CI_SERVER']
+ require 'capybara-screenshot/spinach'
+
+ # Keep only the screenshots generated from the last failing test suite
+ Capybara::Screenshot.prune_strategy = :keep_last_run
+end
diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb
new file mode 100644
index 00000000000..1ab308cfa55
--- /dev/null
+++ b/features/support/db_cleaner.rb
@@ -0,0 +1,11 @@
+require 'database_cleaner'
+
+DatabaseCleaner.strategy = :truncation
+
+Spinach.hooks.before_scenario do
+ DatabaseCleaner.start
+end
+
+Spinach.hooks.after_scenario do
+ DatabaseCleaner.clean
+end
diff --git a/features/support/env.rb b/features/support/env.rb
index be17065ccfd..f34302721ed 100644
--- a/features/support/env.rb
+++ b/features/support/env.rb
@@ -11,40 +11,18 @@ ENV['RAILS_ENV'] = 'test'
require './config/environment'
require 'rspec'
require 'rspec/expectations'
-require 'database_cleaner'
-require 'spinach/capybara'
require 'sidekiq/testing/inline'
+require_relative 'capybara'
+require_relative 'db_cleaner'
+
%w(select2_helper test_env repo_helpers).each do |f|
require Rails.root.join('spec', 'support', f)
end
-Dir["#{Rails.root}/features/steps/shared/*.rb"].each {|file| require file}
+Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
WebMock.allow_net_connect!
-#
-# JS driver
-#
-require 'capybara/poltergeist'
-Capybara.javascript_driver = :poltergeist
-Capybara.register_driver :poltergeist do |app|
- Capybara::Poltergeist::Driver.new(app, js_errors: false, timeout: 90)
-end
-Spinach.hooks.on_tag("javascript") do
- ::Capybara.current_driver = ::Capybara.javascript_driver
-end
-Capybara.default_wait_time = 60
-Capybara.ignore_hidden_elements = false
-
-DatabaseCleaner.strategy = :truncation
-
-Spinach.hooks.before_scenario do
- DatabaseCleaner.start
-end
-
-Spinach.hooks.after_scenario do
- DatabaseCleaner.clean
-end
Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods
diff --git a/features/user.feature b/features/user.feature
index a2167935fd2..69618e929c4 100644
--- a/features/user.feature
+++ b/features/user.feature
@@ -67,3 +67,12 @@ Feature: User
And I should see project "Enterprise"
And I should not see project "Internal"
And I should not see project "Community"
+
+ @javascript
+ Scenario: "John Doe" contribution profile
+ Given I sign in as a user
+ And "John Doe" has contributions
+ When I visit user "John Doe" page
+ Then I should see user "John Doe" page
+ And I should see contributed projects
+ And I should see contributions calendar
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 60858a39407..d2a35c78fc1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -19,7 +19,7 @@ module API
message << " " << trace.join("\n ")
API.logger.add Logger::FATAL, message
- rack_response({ 'message' => '500 Internal Server Error' }, 500)
+ rack_response({ 'message' => '500 Internal Server Error' }.to_json, 500)
end
format :json
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index b52d786e020..592100a7045 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -100,7 +100,8 @@ 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(params[:branch])
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 0de4e720ffe..23270b1c0f4 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -32,7 +32,7 @@ module API
# GET /projects/:id/repository/commits/:sha
get ":id/repository/commits/:sha" do
sha = params[:sha]
- commit = user_project.repository.commit(sha)
+ commit = user_project.commit(sha)
not_found! "Commit" unless commit
present commit, with: Entities::RepoCommitDetail
end
@@ -46,7 +46,7 @@ module API
# GET /projects/:id/repository/commits/:sha/diff
get ":id/repository/commits/:sha/diff" do
sha = params[:sha]
- commit = user_project.repository.commit(sha)
+ commit = user_project.commit(sha)
not_found! "Commit" unless commit
commit.diffs
end
@@ -60,7 +60,7 @@ module API
# GET /projects/:id/repository/commits/:sha/comments
get ':id/repository/commits/:sha/comments' do
sha = params[:sha]
- commit = user_project.repository.commit(sha)
+ commit = user_project.commit(sha)
not_found! 'Commit' unless commit
notes = Note.where(commit_id: commit.id)
present paginate(notes), with: Entities::CommitNote
@@ -81,7 +81,7 @@ module API
required_attributes! [:note]
sha = params[:sha]
- commit = user_project.repository.commit(sha)
+ commit = user_project.commit(sha)
not_found! 'Commit' unless commit
opts = {
note: params[:note],
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 489be210784..b23eff3661c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -20,7 +20,7 @@ module API
class UserFull < User
expose :email
- expose :theme_id, :color_scheme_id, :projects_limit
+ expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
@@ -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
@@ -250,11 +251,11 @@ module API
class Compare < Grape::Entity
expose :commit, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits).last
+ Commit.decorate(compare.commits, nil).last
end
expose :commits, using: Entities::RepoCommit do |compare, options|
- Commit.decorate(compare.commits)
+ Commit.decorate(compare.commits, nil)
end
expose :diffs, using: Entities::RepoDiff do |compare, options|
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 3176ef0e256..e0ea6d7dd1d 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -34,7 +34,7 @@ module API
ref = attrs.delete(:ref)
file_path = attrs.delete(:file_path)
- commit = user_project.repository.commit(ref)
+ commit = user_project.commit(ref)
not_found! 'Commit' unless commit
blob = user_project.repository.blob_at(commit.sha, file_path)
diff --git a/lib/api/group_members.rb b/lib/api/group_members.rb
index c9c9ccbcb2e..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,17 +49,17 @@ 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]
- team_member = group.group_members.find_by(user_id: params[:user_id])
- not_found!('User can not be found') if team_member.nil?
+ group_member = group.group_members.find_by(user_id: params[:user_id])
+ not_found!('User can not be found') if group_member.nil?
- if team_member.update_attributes(access_level: params[:access_level])
- @member = team_member.user
+ if group_member.update_attributes(access_level: params[:access_level])
+ @member = group_member.user
present @member, with: Entities::GroupMember, group: group
else
- handle_member_errors team_member.errors
+ handle_member_errors group_member.errors
end
end
@@ -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..f768c750402 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -20,7 +20,7 @@ module API
present @groups, with: Entities::Group
end
- # Create group. Available only for admin
+ # Create group. Available only for users who can create groups.
#
# Parameters:
# name (required) - The name of the group
@@ -28,7 +28,7 @@ module API
# Example Request:
# POST /groups
post do
- authenticated_as_admin!
+ authorize! :create_group, current_user
required_attributes! [:name, :path]
attrs = attributes_for_keys [:name, :path, :description]
@@ -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/helpers.rb b/lib/api/helpers.rb
index ee678d84c84..85e9081680d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -20,7 +20,7 @@ module API
identifier = sudo_identifier()
# If the sudo is the current user do nothing
- if (identifier && !(@current_user.id == identifier || @current_user.username == identifier))
+ if identifier && !(@current_user.id == identifier || @current_user.username == identifier)
render_api_error!('403 Forbidden: Must be admin to use sudo', 403) unless @current_user.is_admin?
@current_user = User.by_username_or_id(identifier)
not_found!("No user id or username for: #{identifier}") if @current_user.nil?
@@ -33,7 +33,7 @@ module API
identifier ||= params[SUDO_PARAM] ||= env[SUDO_HEADER]
# Regex for integers
- if (!!(identifier =~ /^[0-9]+$/))
+ if !!(identifier =~ /^[0-9]+$/)
identifier.to_i
else
identifier
@@ -173,6 +173,10 @@ module API
end
end
+ def filter_by_iid(items, iid)
+ items.where(iid: iid)
+ end
+
# error helpers
def forbidden!(reason = nil)
@@ -207,7 +211,7 @@ module API
end
def render_validation_error!(model)
- unless model.valid?
+ if model.errors.any?
render_api_error!(model.errors.messages || '400 Bad Request', 400)
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 753d0fcbd98..f98a17773e7 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -17,42 +17,40 @@ module API
post "/allowed" do
status 200
- actor = if params[:key_id]
- Key.find_by(id: params[:key_id])
- elsif params[:user_id]
- User.find_by(id: params[:user_id])
- end
+ actor =
+ if params[:key_id]
+ Key.find_by(id: params[:key_id])
+ elsif params[:user_id]
+ User.find_by(id: params[:user_id])
+ end
unless actor
return Gitlab::GitAccessStatus.new(false, 'No such user or key')
end
project_path = params[:project]
-
+
# Check for *.wiki repositories.
# Strip out the .wiki from the pathname before finding the
# project. This applies the correct project permissions to
# the wiki repository as well.
- access =
- if project_path.end_with?('.wiki')
- project_path.chomp!('.wiki')
- Gitlab::GitAccessWiki.new
- else
- Gitlab::GitAccess.new
- end
+ wiki = project_path.end_with?('.wiki')
+ project_path.chomp!('.wiki') if wiki
project = Project.find_with_namespace(project_path)
if project
- status = access.check(
- actor,
- params[:action],
- project,
- params[:changes]
- )
+ access =
+ if wiki
+ Gitlab::GitAccessWiki.new(actor, project)
+ else
+ Gitlab::GitAccess.new(actor, project)
+ end
+
+ status = access.check(params[:action], params[:changes])
end
- if project && status && status.allowed?
+ if project && access.can_read_project?
status
else
Gitlab::GitAccessStatus.new(false, 'No such project')
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index ff062be6040..c8db93eb778 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -51,6 +51,7 @@ module API
#
# Parameters:
# id (required) - The ID of a project
+ # iid (optional) - Return the project issue having the given `iid`
# state (optional) - Return "opened" or "closed" issues
# labels (optional) - Comma-separated list of label names
# milestone (optional) - Milestone title
@@ -66,10 +67,12 @@ module API
# GET /projects/:id/issues?labels=foo,bar&state=opened
# GET /projects/:id/issues?milestone=1.0.0
# GET /projects/:id/issues?milestone=1.0.0&state=closed
+ # GET /issues?iid=42
get ":id/issues" do
issues = user_project.issues
issues = filter_issues_state(issues, params[:state]) unless params[:state].nil?
issues = filter_issues_labels(issues, params[:labels]) unless params[:labels].nil?
+ issues = filter_by_iid(issues, params[:iid]) unless params[:iid].nil?
unless params[:milestone].nil?
issues = filter_issues_milestone(issues, params[:milestone])
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 25b7857f4b1..2216a12a87a 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -24,6 +24,7 @@ module API
#
# Parameters:
# id (required) - The ID of a project
+ # iid (optional) - Return the project MR having the given `iid`
# state (optional) - Return requests "merged", "opened" or "closed"
# order_by (optional) - Return requests ordered by `created_at` or `updated_at` fields. Default is `created_at`
# sort (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
@@ -36,11 +37,16 @@ module API
# GET /projects/:id/merge_requests?order_by=updated_at
# GET /projects/:id/merge_requests?sort=desc
# GET /projects/:id/merge_requests?sort=asc
+ # GET /projects/:id/merge_requests?iid=42
#
get ":id/merge_requests" do
authorize! :read_merge_request, user_project
merge_requests = user_project.merge_requests
+ unless params[:iid].nil?
+ merge_requests = filter_by_iid(merge_requests, params[:iid])
+ end
+
merge_requests =
case params["state"]
when "opened" then merge_requests.opened
@@ -169,8 +175,8 @@ module API
# Merge MR
#
# Parameters:
- # id (required) - The ID of a project
- # merge_request_id (required) - ID of MR
+ # id (required) - The ID of a project
+ # merge_request_id (required) - ID of MR
# merge_commit_message (optional) - Custom merge commit message
# Example:
# PUT /projects/:id/merge_request/:merge_request_id/merge
@@ -178,14 +184,15 @@ module API
put ":id/merge_request/:merge_request_id/merge" do
merge_request = user_project.merge_requests.find(params[:merge_request_id])
- allowed = ::Gitlab::GitAccess.can_push_to_branch?(current_user, user_project, merge_request.target_branch)
+ allowed = ::Gitlab::GitAccess.new(current_user, user_project).
+ can_push_to_branch?(merge_request.target_branch)
if allowed
if merge_request.unchecked?
merge_request.check_if_can_be_merged
end
- if merge_request.open?
+ if merge_request.open? && !merge_request.work_in_progress?
if merge_request.can_be_merged?
merge_request.automerge!(current_user, params[:merge_commit_message] || merge_request.merge_commit_message)
present merge_request, with: Entities::MergeRequest
@@ -194,7 +201,7 @@ module API
end
else
# Merge request can not be merged
- # because it is already closed/merged
+ # because it is already closed/merged or marked as WIP
not_allowed!
end
else
@@ -208,7 +215,7 @@ module API
# Get a merge request's comments
#
# Parameters:
- # id (required) - The ID of a project
+ # id (required) - The ID of a project
# merge_request_id (required) - ID of MR
# Examples:
# GET /projects/:id/merge_request/:merge_request_id/comments
@@ -224,9 +231,9 @@ module API
# Post comment to merge request
#
# Parameters:
- # id (required) - The ID of a project
+ # id (required) - The ID of a project
# merge_request_id (required) - ID of MR
- # note (required) - Text of comment
+ # note (required) - Text of comment
# Examples:
# POST /projects/:id/merge_request/:merge_request_id/comments
#
diff --git a/lib/api/project_members.rb b/lib/api/project_members.rb
index 73cf062155b..c756bb479fc 100644
--- a/lib/api/project_members.rb
+++ b/lib/api/project_members.rb
@@ -46,19 +46,19 @@ module API
required_attributes! [:user_id, :access_level]
# either the user is already a team member or a new one
- team_member = user_project.team_member_by_id(params[:user_id])
- if team_member.nil?
- team_member = user_project.project_members.new(
+ project_member = user_project.project_member_by_id(params[:user_id])
+ if project_member.nil?
+ project_member = user_project.project_members.new(
user_id: params[:user_id],
access_level: params[:access_level]
)
end
- if team_member.save
- @member = team_member.user
+ if project_member.save
+ @member = project_member.user
present @member, with: Entities::ProjectMember, project: user_project
else
- handle_member_errors team_member.errors
+ handle_member_errors project_member.errors
end
end
@@ -74,14 +74,14 @@ module API
authorize! :admin_project, user_project
required_attributes! [:access_level]
- team_member = user_project.project_members.find_by(user_id: params[:user_id])
- not_found!("User can not be found") if team_member.nil?
+ project_member = user_project.project_members.find_by(user_id: params[:user_id])
+ not_found!("User can not be found") if project_member.nil?
- if team_member.update_attributes(access_level: params[:access_level])
- @member = team_member.user
+ if project_member.update_attributes(access_level: params[:access_level])
+ @member = project_member.user
present @member, with: Entities::ProjectMember, project: user_project
else
- handle_member_errors team_member.errors
+ handle_member_errors project_member.errors
end
end
@@ -94,9 +94,9 @@ module API
# DELETE /projects/:id/members/:user_id
delete ":id/members/:user_id" do
authorize! :admin_project, user_project
- team_member = user_project.project_members.find_by(user_id: params[:user_id])
- unless team_member.nil?
- team_member.destroy
+ project_member = user_project.project_members.find_by(user_id: params[:user_id])
+ unless project_member.nil?
+ project_member.destroy
else
{ message: "Access revoked", id: params[:user_id].to_i }
end
diff --git a/lib/api/project_snippets.rb b/lib/api/project_snippets.rb
index 0c2d282f785..54f2555903f 100644
--- a/lib/api/project_snippets.rb
+++ b/lib/api/project_snippets.rb
@@ -42,21 +42,22 @@ module API
# title (required) - The title of a snippet
# file_name (required) - The name of a snippet file
# code (required) - The content of a snippet
+ # visibility_level (required) - The snippet's visibility
# Example Request:
# POST /projects/:id/snippets
post ":id/snippets" do
authorize! :write_project_snippet, user_project
- required_attributes! [:title, :file_name, :code]
+ required_attributes! [:title, :file_name, :code, :visibility_level]
- attrs = attributes_for_keys [:title, :file_name]
+ attrs = attributes_for_keys [:title, :file_name, :visibility_level]
attrs[:content] = params[:code] if params[:code].present?
- @snippet = user_project.snippets.new attrs
- @snippet.author = current_user
+ @snippet = CreateSnippetService.new(user_project, current_user,
+ attrs).execute
- if @snippet.save
- present @snippet, with: Entities::ProjectSnippet
- else
+ if @snippet.errors.any?
render_validation_error!(@snippet)
+ else
+ present @snippet, with: Entities::ProjectSnippet
end
end
@@ -68,19 +69,22 @@ module API
# title (optional) - The title of a snippet
# file_name (optional) - The name of a snippet file
# code (optional) - The content of a snippet
+ # visibility_level (optional) - The snippet's visibility
# Example Request:
# PUT /projects/:id/snippets/:snippet_id
put ":id/snippets/:snippet_id" do
@snippet = user_project.snippets.find(params[:snippet_id])
authorize! :modify_project_snippet, @snippet
- attrs = attributes_for_keys [:title, :file_name]
+ attrs = attributes_for_keys [:title, :file_name, :visibility_level]
attrs[:content] = params[:code] if params[:code].present?
- if @snippet.update_attributes attrs
- present @snippet, with: Entities::ProjectSnippet
- else
+ UpdateSnippetService.new(user_project, current_user, @snippet,
+ attrs).execute
+ if @snippet.errors.any?
render_validation_error!(@snippet)
+ else
+ present @snippet, with: Entities::ProjectSnippet
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 0677e85beab..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
@@ -233,10 +230,10 @@ module API
::Projects::UpdateService.new(user_project,
current_user, attrs).execute
- if user_project.valid?
- present user_project, with: Entities::Project
- else
+ if user_project.errors.any?
render_validation_error!(user_project)
+ else
+ present user_project, with: Entities::Project
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index b259914a01c..2d96c9666d2 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -62,7 +62,7 @@ module API
ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
path = params[:path] || nil
- commit = user_project.repository.commit(ref)
+ commit = user_project.commit(ref)
not_found!('Tree') unless commit
tree = user_project.repository.tree(commit.id, path)
@@ -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/api/users.rb b/lib/api/users.rb
index 7c8b3250cd0..032a5d76e43 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -61,10 +61,10 @@ module API
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :can_create_group, :admin, :confirm]
- user = User.build_user(attrs)
admin = attrs.delete(:admin)
- user.admin = admin unless admin.nil?
confirm = !(attrs.delete(:confirm) =~ (/(false|f|no|0)$/i))
+ user = User.build_user(attrs)
+ user.admin = admin unless admin.nil?
user.skip_confirmation! unless confirm
identity_attrs = attributes_for_keys [:provider, :extern_uid]
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index ab8db4e9837..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,24 +7,30 @@ 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)
+ Dir.chdir(Gitlab.config.backup.path) do
+ File.open("#{Gitlab.config.backup.path}/backup_information.yml",
+ "w+") do |file|
+ file << s.to_yaml.gsub(/^---\n/,'')
+ end
- File.open("#{Gitlab.config.backup.path}/backup_information.yml", "w+") do |file|
- file << s.to_yaml.gsub(/^---\n/,'')
- end
+ FileUtils.chmod(0700, folders_to_backup)
- # create archive
- $progress.print "Creating backup archive: #{tar_file} ... "
- if Kernel.system('tar', '-cf', tar_file, *BACKUP_CONTENTS)
- $progress.puts "done".green
- else
- puts "creating archive #{tar_file} failed".red
- abort 'Backup failed'
- end
+ # create archive
+ $progress.print "Creating backup archive: #{tar_file} ... "
+ orig_umask = File.umask(0077)
+ if Kernel.system('tar', '-cf', tar_file, *backup_contents)
+ $progress.puts "done".green
+ else
+ puts "creating archive #{tar_file} failed".red
+ abort 'Backup failed'
+ end
+ File.umask(orig_umask)
- upload(tar_file)
+ upload(tar_file)
+ end
end
def upload(tar_file)
@@ -41,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
@@ -51,11 +56,16 @@ module Backup
def cleanup
$progress.print "Deleting tmp directories ... "
- if Kernel.system('rm', '-rf', *BACKUP_CONTENTS)
- $progress.puts "done".green
- else
- puts "deleting tmp directory failed".red
- abort 'Backup failed'
+
+ 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
+ puts "deleting tmp directory '#{dir}' failed".red
+ abort 'Backup failed'
+ end
end
end
@@ -63,19 +73,22 @@ module Backup
# delete backups
$progress.print "Deleting old backups ... "
keep_time = Gitlab.config.backup.keep_time.to_i
- path = Gitlab.config.backup.path
if keep_time > 0
removed = 0
- file_list = Dir.glob(Rails.root.join(path, "*_gitlab_backup.tar"))
- file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ }
- file_list.sort.each do |timestamp|
- if Time.at(timestamp) < (Time.now - keep_time)
- if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar))
- removed += 1
+
+ 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/ }
+ file_list.sort.each do |timestamp|
+ if Time.at(timestamp) < (Time.now - keep_time)
+ if Kernel.system(*%W(rm #{timestamp}_gitlab_backup.tar))
+ removed += 1
+ end
end
end
end
+
$progress.puts "done. (#{removed} removed)".green
else
$progress.puts "skipping".yellow
@@ -88,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"
@@ -102,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
@@ -109,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
@@ -128,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/backup/repository.rb b/lib/backup/repository.rb
index e18bc804437..dfb2da9f84e 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -16,7 +16,7 @@ module Backup
if project.empty_repo?
$progress.puts "[SKIPPED]".cyan
else
- cmd = %W(git --git-dir=#{path_to_repo(project)} bundle create #{path_to_bundle(project)} --all)
+ cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .)
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
$progress.puts "[DONE]".green
@@ -64,7 +64,8 @@ module Backup
project.namespace.ensure_dir_exist if project.namespace
if File.exists?(path_to_bundle(project))
- cmd = %W(git clone --bare #{path_to_bundle(project)} #{path_to_repo(project)})
+ FileUtils.mkdir_p(path_to_repo(project))
+ cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
else
cmd = %W(git init --bare #{path_to_repo(project)})
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/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index ee877e099b1..050b5ba29dd 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -1,3 +1,4 @@
+require_relative 'rack_attack_helpers'
require_relative 'shell_env'
module Grack
@@ -85,25 +86,41 @@ module Grack
user = oauth_access_token_check(login, password)
end
- return user if user.present?
-
- # At this point, we know the credentials were wrong. We let Rack::Attack
- # know there was a failed authentication attempt from this IP. This
- # information is stored in the Rails cache (Redis) and will be used by
- # the Rack::Attack middleware to decide whether to block requests from
- # this IP.
+ # If the user authenticated successfully, we reset the auth failure count
+ # from Rack::Attack for that IP. A client may attempt to authenticate
+ # with a username and blank password first, and only after it receives
+ # a 401 error does it present a password. Resetting the count prevents
+ # false positives from occurring.
+ #
+ # Otherwise, we let Rack::Attack know there was a failed authentication
+ # attempt from this IP. This information is stored in the Rails cache
+ # (Redis) and will be used by the Rack::Attack middleware to decide
+ # whether to block requests from this IP.
config = Gitlab.config.rack_attack.git_basic_auth
- Rack::Attack::Allow2Ban.filter(@request.ip, config) do
- # Unless the IP is whitelisted, return true so that Allow2Ban
- # increments the counter (stored in Rails.cache) for the IP
- if config.ip_whitelist.include?(@request.ip)
- false
+
+ if config.enabled
+ if user
+ # A successful login will reset the auth failure count from this IP
+ Rack::Attack::Allow2Ban.reset(@request.ip, config)
else
- true
+ banned = Rack::Attack::Allow2Ban.filter(@request.ip, config) do
+ # Unless the IP is whitelisted, return true so that Allow2Ban
+ # increments the counter (stored in Rails.cache) for the IP
+ if config.ip_whitelist.include?(@request.ip)
+ false
+ else
+ true
+ end
+ end
+
+ if banned
+ Rails.logger.info "IP #{@request.ip} failed to login " \
+ "as #{login} but has been temporarily banned from Git auth"
+ end
end
end
- nil # No user was found
+ user
end
def authorized_request?
@@ -112,7 +129,7 @@ module Grack
case git_cmd
when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
if user
- Gitlab::GitAccess.new.download_access_check(user, project).allowed?
+ Gitlab::GitAccess.new(user, project).download_access_check.allowed?
elsif project.public?
# Allow clone/fetch for public projects
true
diff --git a/lib/gitlab/backend/rack_attack_helpers.rb b/lib/gitlab/backend/rack_attack_helpers.rb
new file mode 100644
index 00000000000..8538f3f6eca
--- /dev/null
+++ b/lib/gitlab/backend/rack_attack_helpers.rb
@@ -0,0 +1,31 @@
+# rack-attack v4.2.0 doesn't yet support clearing of keys.
+# Taken from https://github.com/kickstarter/rack-attack/issues/113
+class Rack::Attack::Allow2Ban
+ def self.reset(discriminator, options)
+ findtime = options[:findtime] or raise ArgumentError, "Must pass findtime option"
+
+ cache.reset_count("#{key_prefix}:count:#{discriminator}", findtime)
+ cache.delete("#{key_prefix}:ban:#{discriminator}")
+ end
+end
+
+class Rack::Attack::Cache
+ def reset_count(unprefixed_key, period)
+ epoch_time = Time.now.to_i
+ # Add 1 to expires_in to avoid timing error: http://git.io/i1PHXA
+ expires_in = period - (epoch_time % period) + 1
+ key = "#{(epoch_time / period).to_i}:#{unprefixed_key}"
+ delete(key)
+ end
+
+ def delete(unprefixed_key)
+ store.delete("#{prefix}:#{unprefixed_key}")
+ end
+end
+
+class Rack::Attack::StoreProxy::RedisStoreProxy
+ def delete(key, options={})
+ self.del(key)
+ rescue Redis::BaseError
+ end
+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/client.rb b/lib/gitlab/bitbucket_import/client.rb
index c907bebaef6..5b1952b9675 100644
--- a/lib/gitlab/bitbucket_import/client.rb
+++ b/lib/gitlab/bitbucket_import/client.rb
@@ -62,7 +62,7 @@ module Gitlab
end
def find_deploy_key(project_identifier, key)
- JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
+ JSON.parse(api.get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
deploy_key["key"].chomp == key.chomp
end
end
@@ -92,7 +92,7 @@ module Gitlab
end
def bitbucket_options
- OmniAuth::Strategies::Bitbucket.default_options[:client_options]
+ OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys
end
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/commits_calendar.rb b/lib/gitlab/commits_calendar.rb
deleted file mode 100644
index 2f30d238e6b..00000000000
--- a/lib/gitlab/commits_calendar.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Gitlab
- class CommitsCalendar
- attr_reader :timestamps
-
- def initialize(projects, user)
- @timestamps = {}
- date_timestamps = []
-
- projects.reject(&:forked?).each do |project|
- date_timestamps << ProjectContributions.new(project, user).commits_log
- end
-
- # Sumarrize commits from all projects per days
- date_timestamps = date_timestamps.inject do |collection, date|
- collection.merge(date) { |k, old_v, new_v| old_v + new_v }
- end
-
- date_timestamps ||= []
- date_timestamps.each do |date, commits|
- timestamp = Date.parse(date).to_time.to_i.to_s rescue nil
- @timestamps[timestamp] = commits if timestamp
- end
- end
-
- def starting_year
- (Time.now - 1.year).strftime("%Y")
- end
-
- def starting_month
- Date.today.strftime("%m").to_i
- end
- end
-end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
new file mode 100644
index 00000000000..45bb904ed7a
--- /dev/null
+++ b/lib/gitlab/contributions_calendar.rb
@@ -0,0 +1,56 @@
+module Gitlab
+ class ContributionsCalendar
+ attr_reader :timestamps, :projects, :user
+
+ def initialize(projects, user)
+ @projects = projects
+ @user = user
+ end
+
+ def timestamps
+ return @timestamps if @timestamps.present?
+
+ @timestamps = {}
+ date_from = 1.year.ago
+ date_to = Date.today
+
+ events = Event.reorder(nil).contributions.where(author_id: user.id).
+ where("created_at > ?", date_from).where(project_id: projects).
+ group('date(created_at)').
+ select('date(created_at) as date, count(id) as total_amount').
+ map(&:attributes)
+
+ dates = (1.year.ago.to_date..(Date.today + 1.day)).to_a
+
+ dates.each do |date|
+ date_id = date.to_time.to_i.to_s
+ @timestamps[date_id] = 0
+ day_events = events.find { |day_events| day_events["date"] == date }
+
+ if day_events
+ @timestamps[date_id] = day_events["total_amount"]
+ end
+ end
+
+ @timestamps
+ end
+
+ def events_by_date(date)
+ events = Event.contributions.where(author_id: user.id).
+ where("created_at > ? AND created_at < ?", date.beginning_of_day, date.end_of_day).
+ where(project_id: projects)
+
+ events.select do |event|
+ event.push? || event.issue? || event.merge_request?
+ end
+ end
+
+ def starting_year
+ (Time.now - 1.year).strftime("%Y")
+ end
+
+ def starting_month
+ Date.today.strftime("%m").to_i
+ 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 1a25eebe7d1..d8f696d247b 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -5,8 +5,7 @@ module Gitlab
RequestStore.store[key] ||= begin
if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('application_settings')
- RequestStore.store[:current_application_settings] =
- (ApplicationSetting.current || ApplicationSetting.create_from_defaults)
+ ApplicationSetting.current || ApplicationSetting.create_from_defaults
else
fake_application_settings
end
@@ -21,6 +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'],
+ max_attachment_size: Settings.gitlab['max_attachment_size']
)
end
end
diff --git a/lib/gitlab/force_push_check.rb b/lib/gitlab/force_push_check.rb
index eae9773a067..fdb6a35c78d 100644
--- a/lib/gitlab/force_push_check.rb
+++ b/lib/gitlab/force_push_check.rb
@@ -3,11 +3,12 @@ module Gitlab
def self.force_push?(project, oldrev, newrev)
return false if project.empty_repo?
- if oldrev != Gitlab::Git::BLANK_SHA && newrev != Gitlab::Git::BLANK_SHA
+ # Created or deleted branch
+ if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
+ false
+ else
missed_refs, _ = Gitlab::Popen.popen(%W(git --git-dir=#{project.repository.path_to_repo} rev-list #{oldrev} ^#{newrev}))
missed_refs.split("\n").size > 0
- else
- false
end
end
end
diff --git a/lib/gitlab/git.rb b/lib/gitlab/git.rb
index 4a712c6345f..0c350d7c675 100644
--- a/lib/gitlab/git.rb
+++ b/lib/gitlab/git.rb
@@ -1,9 +1,25 @@
module Gitlab
module Git
BLANK_SHA = '0' * 40
+ TAG_REF_PREFIX = "refs/tags/"
+ BRANCH_REF_PREFIX = "refs/heads/"
- def self.extract_ref_name(ref)
- ref.gsub(/\Arefs\/(tags|heads)\//, '')
+ class << self
+ def ref_name(ref)
+ ref.gsub(/\Arefs\/(tags|heads)\//, '')
+ end
+
+ def tag_ref?(ref)
+ ref.start_with?(TAG_REF_PREFIX)
+ end
+
+ def branch_ref?(ref)
+ ref.start_with?(BRANCH_REF_PREFIX)
+ end
+
+ def blank_ref?(ref)
+ ref == BLANK_SHA
+ end
end
end
end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 9b31190a882..bc72b7528d5 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -3,11 +3,34 @@ module Gitlab
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }
PUSH_COMMANDS = %w{ git-receive-pack }
- attr_reader :params, :project, :git_cmd, :user
+ attr_reader :actor, :project
- def self.can_push_to_branch?(user, project, ref)
+ def initialize(actor, project)
+ @actor = actor
+ @project = project
+ end
+
+ def user
+ return @user if defined?(@user)
+
+ @user =
+ case actor
+ when User
+ actor
+ when DeployKey
+ nil
+ when Key
+ actor.user
+ end
+ end
+
+ def deploy_key
+ actor if actor.is_a?(DeployKey)
+ end
+
+ def can_push_to_branch?(ref)
return false unless user
-
+
if project.protected_branch?(ref) &&
!(project.developers_can_push_to_protected_branch?(ref) && project.team.developer?(user))
user.can?(:push_code_to_protected_branches, project)
@@ -16,51 +39,65 @@ module Gitlab
end
end
- def check(actor, cmd, project, changes = nil)
+ def can_read_project?
+ if user
+ user.can?(:read_project, project)
+ elsif deploy_key
+ deploy_key.projects.include?(project)
+ else
+ false
+ end
+ end
+
+ def check(cmd, changes = nil)
case cmd
when *DOWNLOAD_COMMANDS
- download_access_check(actor, project)
+ download_access_check
when *PUSH_COMMANDS
- if actor.is_a? User
- push_access_check(actor, project, changes)
- elsif actor.is_a? DeployKey
- return build_status_object(false, "Deploy key not allowed to push")
- elsif actor.is_a? Key
- push_access_check(actor.user, project, changes)
- else
- raise 'Wrong actor'
- end
+ push_access_check(changes)
else
- return build_status_object(false, "Wrong command")
+ build_status_object(false, "Wrong command")
end
end
- def download_access_check(actor, project)
- if actor.is_a?(User)
- user_download_access_check(actor, project)
- elsif actor.is_a?(DeployKey)
- if actor.projects.include?(project)
- build_status_object(true)
- else
- build_status_object(false, "Deploy key not allowed to access this project")
- end
- elsif actor.is_a? Key
- user_download_access_check(actor.user, project)
+ def download_access_check
+ if user
+ user_download_access_check
+ elsif deploy_key
+ deploy_key_download_access_check
+ else
+ raise 'Wrong actor'
+ end
+ end
+
+ def push_access_check(changes)
+ if user
+ user_push_access_check(changes)
+ elsif deploy_key
+ build_status_object(false, "Deploy key not allowed to push")
else
raise 'Wrong actor'
end
end
- def user_download_access_check(user, project)
- if user && user_allowed?(user) && user.can?(:download_code, project)
+ def user_download_access_check
+ if user && user_allowed? && user.can?(:download_code, project)
build_status_object(true)
else
build_status_object(false, "You don't have access")
end
end
- def push_access_check(user, project, changes)
- unless user && user_allowed?(user)
+ def deploy_key_download_access_check
+ if can_read_project?
+ build_status_object(true)
+ else
+ build_status_object(false, "Deploy key not allowed to access this project")
+ end
+ end
+
+ def user_push_access_check(changes)
+ unless user && user_allowed?
return build_status_object(false, "You don't have access")
end
@@ -76,27 +113,28 @@ module Gitlab
# Iterate over all changes to find if user allowed all of them to be applied
changes.map(&:strip).reject(&:blank?).each do |change|
- status = change_access_check(user, project, change)
+ status = change_access_check(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
return status
end
end
- return build_status_object(true)
+ build_status_object(true)
end
- def change_access_check(user, project, change)
+ def change_access_check(change)
oldrev, newrev, ref = change.split(' ')
- action = if project.protected_branch?(branch_name(ref))
- protected_branch_action(project, oldrev, newrev, branch_name(ref))
- elsif protected_tag?(project, tag_name(ref))
- # Prevent any changes to existing git tag unless user has permissions
- :admin_project
- else
- :push_code
- end
+ action =
+ if project.protected_branch?(branch_name(ref))
+ protected_branch_action(oldrev, newrev, branch_name(ref))
+ elsif protected_tag?(tag_name(ref))
+ # Prevent any changes to existing git tag unless user has permissions
+ :admin_project
+ else
+ :push_code
+ end
if user.can?(action, project)
build_status_object(true)
@@ -105,17 +143,17 @@ module Gitlab
end
end
- def forced_push?(project, oldrev, newrev)
+ def forced_push?(oldrev, newrev)
Gitlab::ForcePushCheck.force_push?(project, oldrev, newrev)
end
private
- def protected_branch_action(project, oldrev, newrev, branch_name)
+ def protected_branch_action(oldrev, newrev, branch_name)
# we dont allow force push to protected branch
- if forced_push?(project, oldrev, newrev)
+ if forced_push?(oldrev, newrev)
:force_push_code_to_protected_branches
- elsif newrev == Gitlab::Git::BLANK_SHA
+ elsif Gitlab::Git.blank_ref?(newrev)
# and we dont allow remove of protected branch
:remove_protected_branches
elsif project.developers_can_push_to_protected_branch?(branch_name)
@@ -125,18 +163,18 @@ module Gitlab
end
end
- def protected_tag?(project, tag_name)
+ def protected_tag?(tag_name)
project.repository.tag_names.include?(tag_name)
end
- def user_allowed?(user)
+ def user_allowed?
Gitlab::UserAccess.allowed?(user)
end
def branch_name(ref)
ref = ref.to_s
- if ref.start_with?('refs/heads')
- ref.sub(%r{\Arefs/heads/}, '')
+ if Gitlab::Git.branch_ref?(ref)
+ Gitlab::Git.ref_name(ref)
else
nil
end
@@ -144,8 +182,8 @@ module Gitlab
def tag_name(ref)
ref = ref.to_s
- if ref.start_with?('refs/tags')
- ref.sub(%r{\Arefs/tags/}, '')
+ if Gitlab::Git.tag_ref?(ref)
+ Gitlab::Git.ref_name(ref)
else
nil
end
diff --git a/lib/gitlab/git_access_wiki.rb b/lib/gitlab/git_access_wiki.rb
index a2177c8d548..73d99b96202 100644
--- a/lib/gitlab/git_access_wiki.rb
+++ b/lib/gitlab/git_access_wiki.rb
@@ -1,6 +1,6 @@
module Gitlab
class GitAccessWiki < GitAccess
- def change_access_check(user, project, change)
+ def change_access_check(change)
if user.can?(:write_wiki, project)
build_status_object(true)
else
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 676d226bddd..270cbcd9ccd 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -46,7 +46,7 @@ module Gitlab
end
def github_options
- OmniAuth::Strategies::GitHub.default_options[:client_options]
+ OmniAuth::Strategies::GitHub.default_options[:client_options].symbolize_keys
end
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 ecf4ff94e39..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
@@ -71,7 +75,7 @@ module Gitlab
end
def gitlab_options
- OmniAuth::Strategies::GitLab.default_options[:client_options]
+ OmniAuth::Strategies::GitLab.default_options[:client_options].symbolize_keys
end
end
end
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..1fa89dba448 100644
--- a/lib/gitlab/gitorious_import/client.rb
+++ b/lib/gitlab/gitorious_import/client.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def repos
- @repos ||= repo_names.map { |full_name| Repository.new(full_name) }
+ @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) }
end
def repo(id)
@@ -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..890bd9a3554
--- /dev/null
+++ b/lib/gitlab/google_code_import/client.rb
@@ -0,0 +1,52 @@
+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 incompatible_repos
+ @incompatible_repos ||= raw_data["projects"].map { |raw_repo| GoogleCodeImport::Repository.new(raw_repo) }.reject(&: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..70bfe059776
--- /dev/null
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -0,0 +1,377 @@
+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 do |hash, user|
+ # Replace ... by \.\.\., so `johnsm...@gmail.com` isn't autolinked.
+ Client.mask_email(user).sub("...", "\\.\\.\\.")
+ end
+
+ 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 = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ s = s.gsub(/([Cc]omment) #([0-9]+)/, '\1 \2')
+ s
+ end
+
+ def escape_for_markdown(s)
+ # No headings and lists
+ s = s.gsub(/^#/, "\\#")
+ s = s.gsub(/^-/, "\\-")
+
+ # No inline code
+ s = s.gsub("`", "\\`")
+
+ # Carriage returns make me sad
+ s = s.gsub("\r", "")
+
+ # Markdown ignores single newlines, but we need them as <br />.
+ 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)
+
+ deleted = name.start_with?("-")
+ name = name[1..-1] if deleted
+
+ text =
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ text = "~~#{text}~~" if deleted
+ text
+ end
+ updates << "*Blocked on: #{blocked_ons.join(", ")}*"
+ end
+
+ if raw_updates.has_key?("blocking")
+ blockings = raw_updates["blocking"].map do |raw_blocked_on|
+ name, id = raw_blocked_on.split(":", 2)
+
+ deleted = name.start_with?("-")
+ name = name[1..-1] if deleted
+
+ text =
+ if name == project.import_source
+ "##{id}"
+ else
+ "#{project.namespace.path}/#{name}##{id}"
+ end
+ text = "~~#{text}~~" if deleted
+ text
+ 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} (imported from Google Code)*"
+ 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/identifier.rb b/lib/gitlab/identifier.rb
index 6e4de197eeb..3e5d728f3bc 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -5,7 +5,7 @@ module Gitlab
def identify(identifier, project, newrev)
if identifier.blank?
# Local push from gitlab
- email = project.repository.commit(newrev).author_email rescue nil
+ email = project.commit(newrev).author_email rescue nil
User.find_by(email: email) if email
elsif identifier =~ /\Auser-\d+\Z/
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/access.rb b/lib/gitlab/ldap/access.rb
index 0c85acf7e69..960fb3849b4 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -34,7 +34,15 @@ module Gitlab
def allowed?
if Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
return true unless ldap_config.active_directory
- !Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+
+ # Block user in GitLab if he/she was blocked in AD
+ if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+ user.block unless user.blocked?
+ false
+ else
+ user.activate if user.blocked?
+ true
+ end
else
false
end
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
index 8af2c74e959..bad683c6511 100644
--- a/lib/gitlab/ldap/authentication.rb
+++ b/lib/gitlab/ldap/authentication.rb
@@ -1,4 +1,4 @@
-# This calls helps to authenticate to LDAP by providing username and password
+# These calls help to authenticate to LDAP by providing username and password
#
# Since multiple LDAP servers are supported, it will loop through all of them
# until a valid bind is found
@@ -50,7 +50,7 @@ module Gitlab
end
def user_filter(login)
- filter = Net::LDAP::Filter.eq(config.uid, login)
+ filter = Net::LDAP::Filter.equals(config.uid, login)
# Apply LDAP user filter if present
if config.user_filter.present?
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/person.rb b/lib/gitlab/ldap/person.rb
index 3e0b3e6cbf8..b81f3e8e8f5 100644
--- a/lib/gitlab/ldap/person.rb
+++ b/lib/gitlab/ldap/person.rb
@@ -9,6 +9,7 @@ module Gitlab
attr_accessor :entry, :provider
def self.find_by_uid(uid, adapter)
+ uid = Net::LDAP::Filter.escape(uid)
adapter.user(adapter.config.uid, uid)
end
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 2dfa18da482..63294aa54c0 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -1,50 +1,43 @@
require 'html/pipeline'
-require 'html/pipeline/gitlab'
+require 'task_list/filter'
module Gitlab
# Custom parser for GitLab-flavored Markdown
#
- # It replaces references in the text with links to the appropriate items in
- # GitLab.
- #
- # Supported reference formats are:
- # * @foo for team members
- # * #123 for issues
- # * #JIRA-123 for Jira issues
- # * !123 for merge requests
- # * $123 for snippets
- # * 123456 for commits
- # * 123456...7890123 for commit ranges (comparisons)
- #
- # It also parses Emoji codes to insert images. See
- # http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
- #
- # Examples
- #
- # >> gfm("Hey @david, can you fix this?")
- # => "Hey <a href="/u/david">@david</a>, can you fix this?"
- #
- # >> gfm("Commit 35d5f7c closes #1234")
- # => "Commit <a href="/gitlab/commits/35d5f7c">35d5f7c</a> closes <a href="/gitlab/issues/1234">#1234</a>"
- #
- # >> gfm(":trollface:")
- # => "<img alt=\":trollface:\" class=\"emoji\" src=\"/images/trollface.png" title=\":trollface:\" />
+ # See the files in `lib/gitlab/markdown/` for specific processing information.
module Markdown
- include IssuesHelper
+ # Provide autoload paths for filters to prevent a circular dependency error
+ autoload :AutolinkFilter, 'gitlab/markdown/autolink_filter'
+ autoload :CommitRangeReferenceFilter, 'gitlab/markdown/commit_range_reference_filter'
+ autoload :CommitReferenceFilter, 'gitlab/markdown/commit_reference_filter'
+ autoload :EmojiFilter, 'gitlab/markdown/emoji_filter'
+ autoload :ExternalIssueReferenceFilter, 'gitlab/markdown/external_issue_reference_filter'
+ autoload :IssueReferenceFilter, 'gitlab/markdown/issue_reference_filter'
+ autoload :LabelReferenceFilter, 'gitlab/markdown/label_reference_filter'
+ autoload :MergeRequestReferenceFilter, 'gitlab/markdown/merge_request_reference_filter'
+ autoload :SanitizationFilter, 'gitlab/markdown/sanitization_filter'
+ autoload :SnippetReferenceFilter, 'gitlab/markdown/snippet_reference_filter'
+ autoload :TableOfContentsFilter, 'gitlab/markdown/table_of_contents_filter'
+ autoload :UserReferenceFilter, 'gitlab/markdown/user_reference_filter'
- attr_reader :html_options
-
- def gfm_with_tasks(text, project = @project, html_options = {})
- text = gfm(text, project, html_options)
- parse_tasks(text)
+ # Public: Parse the provided text with GitLab-Flavored Markdown
+ #
+ # text - the source text
+ # 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)
end
# 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
+ # options - A Hash of options used to customize output (default: {}):
+ # :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(text, project = @project, html_options = {})
+ def gfm_with_options(text, options = {}, project = @project, html_options = {})
return text if text.nil?
# Duplicate the string so we don't alter the original, then call to_str
@@ -52,299 +45,67 @@ module Gitlab
# for gsub calls to work as we need them to.
text = text.dup.to_str
- @html_options = html_options
-
- # Extract pre blocks so they are not altered
- # from http://github.github.com/github-flavored-markdown/
- text.gsub!(%r{<pre>.*?</pre>|<code>.*?</code>}m) { |match| extract_piece(match) }
- # Extract links with probably parsable hrefs
- text.gsub!(%r{<a.*?>.*?</a>}m) { |match| extract_piece(match) }
- # Extract images with probably parsable src
- text.gsub!(%r{<img.*?>}m) { |match| extract_piece(match) }
-
- # TODO: add popups with additional information
+ options.reverse_merge!(
+ xhtml: false,
+ reference_only_path: true
+ )
- text = parse(text, project)
+ pipeline = HTML::Pipeline.new(filters)
- # Insert pre block extractions
- text.gsub!(/\{gfm-extraction-(\h{32})\}/) do
- insert_piece($1)
- end
+ context = {
+ # EmojiFilter
+ asset_root: Gitlab.config.gitlab.url,
+ asset_host: Gitlab::Application.config.asset_host,
- # Used markdown pipelines in GitLab:
- # GitlabEmojiFilter - performs emoji replacement.
- #
- # see https://gitlab.com/gitlab-org/html-pipeline-gitlab for more filters
- filters = [
- HTML::Pipeline::Gitlab::GitlabEmojiFilter
- ]
+ # TableOfContentsFilter
+ no_header_anchors: options[:no_header_anchors],
- markdown_context = {
- asset_root: Gitlab.config.gitlab.url,
- asset_host: Gitlab::Application.config.asset_host
+ # ReferenceFilter
+ current_user: current_user,
+ only_path: options[:reference_only_path],
+ project: project,
+ reference_class: html_options[:class]
}
- markdown_pipeline = HTML::Pipeline::Gitlab.new(filters).pipeline
-
- result = markdown_pipeline.call(text, markdown_context)
- text = result[:output].to_html(save_with: 0)
-
- allowed_attributes = ActionView::Base.sanitized_allowed_attributes
- allowed_tags = ActionView::Base.sanitized_allowed_tags
-
- sanitize text.html_safe,
- attributes: allowed_attributes + %w(id class style),
- tags: allowed_tags + %w(table tr td th)
- end
-
- private
-
- def extract_piece(text)
- @extractions ||= {}
-
- md5 = Digest::MD5.hexdigest(text)
- @extractions[md5] = text
- "{gfm-extraction-#{md5}}"
- end
-
- def insert_piece(id)
- @extractions[id]
- end
-
- # Private: Parses text for references and emoji
- #
- # text - Text to parse
- #
- # Returns parsed text
- def parse(text, project = @project)
- parse_references(text, project) if project
+ result = pipeline.call(text, context)
- text
- end
-
- NAME_STR = '[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*'
- PROJ_STR = "(?<project>#{NAME_STR}/#{NAME_STR})"
-
- REFERENCE_PATTERN = %r{
- (?<prefix>\W)? # Prefix
- ( # Reference
- @(?<user>#{NAME_STR}) # User name
- |~(?<label>\d+) # Label ID
- |(?<issue>([A-Z\-]+-)\d+) # JIRA Issue ID
- |#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
- |#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
- |\$(?<snippet>\d+) # Snippet ID
- |(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
- |(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
- |(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit
- )
- (?<suffix>\W)? # Suffix
- }x.freeze
-
- TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze
-
- def parse_references(text, project = @project)
- # parse reference links
- text.gsub!(REFERENCE_PATTERN) do |match|
- type = TYPES.select{|t| !$~[t].nil?}.first
-
- actual_project = project
- project_prefix = nil
- project_path = $LAST_MATCH_INFO[:project]
- if project_path
- actual_project = ::Project.find_with_namespace(project_path)
- project_prefix = project_path
- end
-
- parse_result($LAST_MATCH_INFO, type,
- actual_project, project_prefix) || match
+ save_options = 0
+ if options[:xhtml]
+ save_options |= Nokogiri::XML::Node::SaveOptions::AS_XHTML
end
- end
-
- # Called from #parse_references. Attempts to build a gitlab reference
- # link. Returns nil if +type+ is nil, if the match string is an HTML
- # entity, if the reference is invalid, or if the matched text includes an
- # invalid project path.
- def parse_result(match_info, type, project, project_prefix)
- prefix = match_info[:prefix]
- suffix = match_info[:suffix]
- return nil if html_entity?(prefix, suffix) || type.nil?
- return nil if project.nil? && !project_prefix.nil?
+ text = result[:output].to_html(save_with: save_options)
- identifier = match_info[type]
- ref_link = reference_link(type, identifier, project, project_prefix)
-
- if ref_link
- "#{prefix}#{ref_link}#{suffix}"
- else
- nil
- end
+ text.html_safe
end
- # Return true if the +prefix+ and +suffix+ indicate that the matched string
- # is an HTML entity like &amp;
- def html_entity?(prefix, suffix)
- prefix && suffix && prefix[0] == '&' && suffix[-1] == ';'
- end
+ private
- # Private: Dispatches to a dedicated processing method based on reference
+ # Filters used in our pipeline
#
- # reference - Object reference ("@1234", "!567", etc.)
- # identifier - Object identifier (Issue ID, SHA hash, etc.)
+ # SanitizationFilter should come first so that all generated reference HTML
+ # goes through untouched.
#
- # Returns string rendered by the processing method
- def reference_link(type, identifier, project = @project, prefix_text = nil)
- send("reference_#{type}", identifier, project, prefix_text)
- end
-
- def reference_user(identifier, project = @project, _ = nil)
- options = html_options.merge(
- class: "gfm gfm-team_member #{html_options[:class]}"
- )
-
- if identifier == "all"
- link_to("@all", namespace_project_url(project.namespace, project), options)
- elsif namespace = Namespace.find_by(path: identifier)
- url =
- if namespace.type == "Group"
- group_url(identifier)
- else
- user_url(identifier)
- end
-
- link_to("@#{identifier}", url, options)
- end
- end
-
- def reference_label(identifier, project = @project, _ = nil)
- if label = project.labels.find_by(id: identifier)
- 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
- )
- end
- end
-
- 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)
- title = title_for_issue(identifier, project)
- options = html_options.merge(
- title: "Issue: #{title}",
- class: "gfm gfm-issue #{html_options[:class]}"
- )
-
- link_to("#{prefix_text}##{identifier}", url, options)
- end
- else
- if project.external_issue_tracker.present?
- reference_external_issue(identifier, project,
- prefix_text)
- end
- end
- end
-
- def reference_merge_request(identifier, project = @project,
- prefix_text = nil)
- if merge_request = project.merge_requests.find_by(iid: identifier)
- 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)
- end
- end
-
- def reference_snippet(identifier, project = @project, _ = nil)
- if snippet = project.snippets.find_by(id: identifier)
- 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
- )
- end
- end
-
- def reference_commit(identifier, project = @project, prefix_text = nil)
- if project.valid_repo? && commit = project.repository.commit(identifier)
- 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
- )
- end
- end
-
- def reference_commit_range(identifier, project = @project, prefix_text = nil)
- from_id, to_id = identifier.split(/\.{2,3}/, 2)
-
- inclusive = identifier !~ /\.{3}/
- from_id << "^" if inclusive
-
- if project.valid_repo? &&
- from = project.repository.commit(from_id) &&
- to = project.repository.commit(to_id)
-
- options = html_options.merge(
- title: "Commits #{from_id} through #{to_id}",
- class: "gfm gfm-commit_range #{html_options[:class]}"
- )
- prefix_text = "#{prefix_text}@" if prefix_text
-
- link_to(
- "#{prefix_text}#{identifier}",
- namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id),
- options
- )
- end
- end
-
- def reference_external_issue(identifier, project = @project,
- prefix_text = nil)
- url = url_for_issue(identifier, project)
- title = project.external_issue_tracker.title
-
- options = html_options.merge(
- title: "Issue in #{title}",
- class: "gfm gfm-issue #{html_options[:class]}"
- )
- link_to("#{prefix_text}##{identifier}", url, options)
- end
-
- # Turn list items that start with "[ ]" into HTML checkbox inputs.
- def parse_tasks(text)
- li_tag = '<li class="task-list-item">'
- unchecked_box = '<input type="checkbox" value="on" disabled />'
- checked_box = unchecked_box.sub(/\/>$/, 'checked="checked" />')
-
- # Regexp captures don't seem to work when +text+ is an
- # ActiveSupport::SafeBuffer, hence the `String.new`
- String.new(text).gsub(Taskable::TASK_PATTERN_HTML) do
- checked = $LAST_MATCH_INFO[:checked].downcase == 'x'
-
- if checked
- "#{li_tag}#{checked_box}"
- else
- "#{li_tag}#{unchecked_box}"
- end
- end
+ # See https://github.com/jch/html-pipeline#filters for more filters.
+ def filters
+ [
+ Gitlab::Markdown::SanitizationFilter,
+
+ Gitlab::Markdown::EmojiFilter,
+ Gitlab::Markdown::TableOfContentsFilter,
+ Gitlab::Markdown::AutolinkFilter,
+
+ Gitlab::Markdown::UserReferenceFilter,
+ Gitlab::Markdown::IssueReferenceFilter,
+ Gitlab::Markdown::ExternalIssueReferenceFilter,
+ Gitlab::Markdown::MergeRequestReferenceFilter,
+ Gitlab::Markdown::SnippetReferenceFilter,
+ Gitlab::Markdown::CommitRangeReferenceFilter,
+ Gitlab::Markdown::CommitReferenceFilter,
+ Gitlab::Markdown::LabelReferenceFilter,
+
+ TaskList::Filter
+ ]
end
end
end
diff --git a/lib/gitlab/markdown/autolink_filter.rb b/lib/gitlab/markdown/autolink_filter.rb
new file mode 100644
index 00000000000..4e14a048cfb
--- /dev/null
+++ b/lib/gitlab/markdown/autolink_filter.rb
@@ -0,0 +1,100 @@
+require 'html/pipeline/filter'
+require 'uri'
+
+module Gitlab
+ module Markdown
+ # HTML Filter for auto-linking URLs in HTML.
+ #
+ # Based on HTML::Pipeline::AutolinkFilter
+ #
+ # Context options:
+ # :autolink - Boolean, skips all processing done by this filter when false
+ # :link_attr - Hash of attributes for the generated links
+ #
+ class AutolinkFilter < HTML::Pipeline::Filter
+ include ActionView::Helpers::TagHelper
+
+ # Pattern to match text that should be autolinked.
+ #
+ # A URI scheme begins with a letter and may contain letters, numbers,
+ # plus, period and hyphen. Schemes are case-insensitive but we're being
+ # picky here and allowing only lowercase for autolinks.
+ #
+ # See http://en.wikipedia.org/wiki/URI_scheme
+ #
+ # The negative lookbehind ensures that users can paste a URL followed by a
+ # period or comma for punctuation without those characters being included
+ # in the generated link.
+ #
+ # Rubular: http://rubular.com/r/cxjPyZc7Sb
+ LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://\S+)(?<!,|\.)}
+
+ # Text matching LINK_PATTERN inside these elements will not be linked
+ IGNORE_PARENTS = %w(a code kbd pre script style).to_set
+
+ def call
+ return doc if context[:autolink] == false
+
+ rinku_parse
+ text_parse
+ end
+
+ private
+
+ # Run the text through Rinku as a first pass
+ #
+ # This will quickly autolink http(s) and ftp links.
+ #
+ # `@doc` will be re-parsed with the HTML String from Rinku.
+ def rinku_parse
+ # Convert the options from a Hash to a String that Rinku expects
+ options = tag_options(link_options)
+
+ # NOTE: We don't parse email links because it will erroneously match
+ # external Commit and CommitRange references.
+ #
+ # The final argument tells Rinku to link short URLs that don't include a
+ # period (e.g., http://localhost:3000/)
+ rinku = Rinku.auto_link(html, :urls, options, IGNORE_PARENTS.to_a, 1)
+
+ # Rinku returns a String, so parse it back to a Nokogiri::XML::Document
+ # for further processing.
+ @doc = parse_html(rinku)
+ end
+
+ # Autolinks any text matching LINK_PATTERN that Rinku didn't already
+ # replace
+ def text_parse
+ search_text_nodes(doc).each do |node|
+ content = node.to_html
+
+ next if has_ancestor?(node, IGNORE_PARENTS)
+ next unless content.match(LINK_PATTERN)
+
+ # If Rinku didn't link this, there's probably a good reason, so we'll
+ # skip it too
+ next if content.start_with?(*%w(http https ftp))
+
+ html = autolink_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ def autolink_filter(text)
+ text.gsub(LINK_PATTERN) do |match|
+ options = link_options.merge(href: match)
+ content_tag(:a, match, options)
+ end
+ end
+
+ def link_options
+ @link_options ||= context[:link_attr] || {}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
new file mode 100644
index 00000000000..8764f7e474f
--- /dev/null
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -0,0 +1,84 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces commit range references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitRangeReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find commit range references in text
+ #
+ # CommitRangeReferenceFilter.references_in(text) do |match, commit_range, project_ref|
+ # "<a href=...>#{commit_range}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the String commit range, and an optional String
+ # of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(COMMIT_RANGE_PATTERN) do |match|
+ yield match, $~[:commit_range], $~[:project]
+ end
+ end
+
+ def initialize(*args)
+ super
+
+ @commit_map = {}
+ end
+
+ # Pattern used to extract commit range references from text
+ #
+ # This pattern supports cross-project references.
+ COMMIT_RANGE_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit_range>#{CommitRange::PATTERN})/
+
+ def call
+ replace_text_nodes_matching(COMMIT_RANGE_PATTERN) do |content|
+ commit_range_link_filter(content)
+ end
+ end
+
+ # Replace commit range references in text with links to compare the commit
+ # ranges.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with commit range references replaced with links. All
+ # links have `gfm` and `gfm-commit_range` class names attached for
+ # styling.
+ def commit_range_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ range = CommitRange.new(id, project)
+
+ if range.valid_commits?
+ push_result(:commit_range, range)
+
+ url = url_for_commit_range(project, range)
+
+ title = range.reference_title
+ klass = reference_class(:commit_range)
+
+ project_ref += '@' if project_ref
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}#{range}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_commit_range(project, range)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_compare_url(project.namespace, project,
+ range.to_param.merge(only_path: context[:only_path]))
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
new file mode 100644
index 00000000000..b20b29f5d0c
--- /dev/null
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -0,0 +1,82 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces commit references with links.
+ #
+ # This filter supports cross-project references.
+ class CommitReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find commit references in text
+ #
+ # CommitReferenceFilter.references_in(text) do |match, commit, project_ref|
+ # "<a href=...>#{commit}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the String commit identifier, and an optional
+ # String of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(COMMIT_PATTERN) do |match|
+ yield match, $~[:commit], $~[:project]
+ end
+ end
+
+ # Pattern used to extract commit references from text
+ #
+ # The SHA1 sum can be between 6 and 40 hex characters.
+ #
+ # This pattern supports cross-project references.
+ COMMIT_PATTERN = /(#{PROJECT_PATTERN}@)?(?<commit>\h{6,40})/
+
+ def call
+ replace_text_nodes_matching(COMMIT_PATTERN) do |content|
+ commit_link_filter(content)
+ end
+ end
+
+ # Replace commit references in text with links to the commit specified.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with commit references replaced with links. All links
+ # have `gfm` and `gfm-commit` class names attached for styling.
+ def commit_link_filter(text)
+ self.class.references_in(text) do |match, commit_ref, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if commit = commit_from_ref(project, commit_ref)
+ push_result(:commit, commit)
+
+ url = url_for_commit(project, commit)
+
+ title = escape_once(commit.link_title)
+ klass = reference_class(:commit)
+
+ project_ref += '@' if project_ref
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}#{commit.short_id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def commit_from_ref(project, commit_ref)
+ if project && project.valid_repo?
+ project.commit(commit_ref)
+ end
+ end
+
+ def url_for_commit(project, commit)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_commit_url(project.namespace, project, commit,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/cross_project_reference.rb b/lib/gitlab/markdown/cross_project_reference.rb
new file mode 100644
index 00000000000..c436fabd658
--- /dev/null
+++ b/lib/gitlab/markdown/cross_project_reference.rb
@@ -0,0 +1,32 @@
+module Gitlab
+ module Markdown
+ # Common methods for ReferenceFilters that support an optional cross-project
+ # reference.
+ module CrossProjectReference
+ NAMING_PATTERN = Gitlab::Regex::NAMESPACE_REGEX_STR
+ PROJECT_PATTERN = "(?<project>#{NAMING_PATTERN}/#{NAMING_PATTERN})"
+
+ # Given a cross-project reference string, get the Project record
+ #
+ # Defaults to value of `context[:project]` if:
+ # * No reference is given OR
+ # * Reference given doesn't exist
+ #
+ # ref - String reference.
+ #
+ # Returns a Project, or nil if the reference can't be accessed
+ def project_from_ref(ref)
+ return context[:project] unless ref
+
+ other = Project.find_with_namespace(ref)
+ return nil unless other && user_can_reference_project?(other)
+
+ other
+ end
+
+ def user_can_reference_project?(project, user = context[:current_user])
+ Ability.abilities.allowed?(user, :read_project, project)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/emoji_filter.rb b/lib/gitlab/markdown/emoji_filter.rb
new file mode 100644
index 00000000000..6794ab9c897
--- /dev/null
+++ b/lib/gitlab/markdown/emoji_filter.rb
@@ -0,0 +1,79 @@
+require 'gitlab_emoji'
+require 'html/pipeline/filter'
+require 'action_controller'
+
+module Gitlab
+ module Markdown
+ # HTML filter that replaces :emoji: with images.
+ #
+ # Based on HTML::Pipeline::EmojiFilter
+ #
+ # Context options:
+ # :asset_root
+ # :asset_host
+ class EmojiFilter < HTML::Pipeline::Filter
+ IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
+
+ def call
+ search_text_nodes(doc).each do |node|
+ content = node.to_html
+ next unless content.include?(':')
+ next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
+
+ html = emoji_image_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Replace :emoji: with corresponding images.
+ #
+ # text - String text to replace :emoji: in.
+ #
+ # Returns a String with :emoji: replaced with images.
+ def emoji_image_filter(text)
+ text.gsub(emoji_pattern) do |match|
+ name = $1
+ "<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{emoji_url(name)}' height='20' width='20' align='absmiddle' />"
+ end
+ end
+
+ private
+
+ def emoji_url(name)
+ emoji_path = "emoji/#{emoji_filename(name)}"
+ if context[:asset_host]
+ # Asset host is specified.
+ url_to_image(emoji_path)
+ elsif context[:asset_root]
+ # Gitlab url is specified
+ File.join(context[:asset_root], url_to_image(emoji_path))
+ else
+ # All other cases
+ url_to_image(emoji_path)
+ end
+ end
+
+ def url_to_image(image)
+ ActionController::Base.helpers.url_to_image(image)
+ end
+
+ # Build a regexp that matches all valid :emoji: names.
+ def self.emoji_pattern
+ @emoji_pattern ||= /:(#{Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
+ end
+
+ def emoji_pattern
+ self.class.emoji_pattern
+ end
+
+ def emoji_filename(name)
+ "#{Emoji.emoji_filename(name)}.png"
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/external_issue_reference_filter.rb b/lib/gitlab/markdown/external_issue_reference_filter.rb
new file mode 100644
index 00000000000..0fc3f4cca06
--- /dev/null
+++ b/lib/gitlab/markdown/external_issue_reference_filter.rb
@@ -0,0 +1,63 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces external issue tracker references with links.
+ # References are ignored if the project doesn't use an external issue
+ # tracker.
+ class ExternalIssueReferenceFilter < ReferenceFilter
+ # Public: Find `JIRA-123` issue references in text
+ #
+ # ExternalIssueReferenceFilter.references_in(text) do |match, issue|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match and the String issue reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(ISSUE_PATTERN) do |match|
+ yield match, $~[:issue]
+ end
+ end
+
+ # Pattern used to extract `JIRA-123` issue references from text
+ ISSUE_PATTERN = /(?<issue>([A-Z\-]+-)\d+)/
+
+ def call
+ # Early return if the project isn't using an external tracker
+ return doc if project.nil? || project.default_issues_tracker?
+
+ replace_text_nodes_matching(ISSUE_PATTERN) do |content|
+ issue_link_filter(content)
+ end
+ end
+
+ # Replace `JIRA-123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `JIRA-123` references replaced with links. All
+ # links have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, issue|
+ url = url_for_issue(issue, project, only_path: context[:only_path])
+
+ title = escape_once("Issue in #{project.external_issue_tracker.title}")
+ klass = reference_class(:issue)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{issue}</a>)
+ end
+ end
+
+ def url_for_issue(*args)
+ IssuesHelper.url_for_issue(*args)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/issue_reference_filter.rb b/lib/gitlab/markdown/issue_reference_filter.rb
new file mode 100644
index 00000000000..1e885615163
--- /dev/null
+++ b/lib/gitlab/markdown/issue_reference_filter.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces issue references with links. References to
+ # issues that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class IssueReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find `#123` issue references in text
+ #
+ # IssueReferenceFilter.references_in(text) do |match, issue, project_ref|
+ # "<a href=...>##{issue}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer issue ID, and an optional String of
+ # the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(ISSUE_PATTERN) do |match|
+ yield match, $~[:issue].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `#123` issue references from text
+ #
+ # This pattern supports cross-project references.
+ ISSUE_PATTERN = /#{PROJECT_PATTERN}?\#(?<issue>([a-zA-Z\-]+-)?\d+)/
+
+ def call
+ replace_text_nodes_matching(ISSUE_PATTERN) do |content|
+ issue_link_filter(content)
+ end
+ end
+
+ # Replace `#123` issue references in text with links to the referenced
+ # issue's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `#123` references replaced with links. All links
+ # have `gfm` and `gfm-issue` class names attached for styling.
+ def issue_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project && issue = project.get_issue(id)
+ push_result(:issue, issue)
+
+ url = url_for_issue(id, project, only_path: context[:only_path])
+
+ title = escape_once("Issue: #{issue.title}")
+ klass = reference_class(:issue)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}##{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_issue(*args)
+ IssuesHelper.url_for_issue(*args)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
new file mode 100644
index 00000000000..a357f28458d
--- /dev/null
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -0,0 +1,96 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces label references with links.
+ class LabelReferenceFilter < ReferenceFilter
+ # Public: Find label references in text
+ #
+ # LabelReferenceFilter.references_in(text) do |match, id, name|
+ # "<a href=...>#{Label.find(id)}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, an optional Integer label ID, and an optional
+ # String label name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(LABEL_PATTERN) do |match|
+ yield match, $~[:label_id].to_i, $~[:label_name]
+ end
+ end
+
+ # Pattern used to extract label references from text
+ #
+ # TODO (rspeicher): Limit to double quotes (meh) or disallow single quotes in label names (bad).
+ LABEL_PATTERN = %r{
+ ~(
+ (?<label_id>\d+) | # Integer-based label ID, or
+ (?<label_name>
+ [A-Za-z0-9_-]+ | # String-based single-word label title
+ ['"][^&\?,]+['"] # String-based multi-word label surrounded in quotes
+ )
+ )
+ }x
+
+ def call
+ replace_text_nodes_matching(LABEL_PATTERN) do |content|
+ label_link_filter(content)
+ end
+ end
+
+ # Replace label references in text with links to the label specified.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with label references replaced with links. All links
+ # have `gfm` and `gfm-label` class names attached for styling.
+ def label_link_filter(text)
+ project = context[:project]
+
+ self.class.references_in(text) do |match, id, name|
+ params = label_params(id, name)
+
+ if label = project.labels.find_by(params)
+ push_result(:label, label)
+
+ url = url_for_label(project, label)
+ klass = reference_class(:label)
+
+ %(<a href="#{url}"
+ class="#{klass}">#{render_colored_label(label)}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_label(project, label)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_issues_path(project.namespace, project,
+ label_name: label.name,
+ only_path: context[:only_path])
+ end
+
+ def render_colored_label(label)
+ LabelsHelper.render_colored_label(label)
+ end
+
+ # Parameters to pass to `Label.find_by` based on the given arguments
+ #
+ # id - Integer ID to pass. If present, returns {id: id}
+ # name - String name to pass. If `id` is absent, finds by name without
+ # surrounding quotes.
+ #
+ # Returns a Hash.
+ def label_params(id, name)
+ if id > 0
+ { id: id }
+ else
+ # TODO (rspeicher): Don't strip single quotes if we decide to only use double quotes for surrounding.
+ { name: name.tr('\'"', '') }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
new file mode 100644
index 00000000000..740d72abb36
--- /dev/null
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -0,0 +1,74 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces merge request references with links. References
+ # to merge requests that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class MergeRequestReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find `!123` merge request references in text
+ #
+ # MergeRequestReferenceFilter.references_in(text) do |match, merge_request, project_ref|
+ # "<a href=...>##{merge_request}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer merge request ID, and an optional
+ # String of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(MERGE_REQUEST_PATTERN) do |match|
+ yield match, $~[:merge_request].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `!123` merge request references from text
+ #
+ # This pattern supports cross-project references.
+ MERGE_REQUEST_PATTERN = /#{PROJECT_PATTERN}?!(?<merge_request>\d+)/
+
+ def call
+ replace_text_nodes_matching(MERGE_REQUEST_PATTERN) do |content|
+ merge_request_link_filter(content)
+ end
+ end
+
+ # Replace `!123` merge request references in text with links to the
+ # referenced merge request's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `!123` references replaced with links. All links
+ # have `gfm` and `gfm-merge_request` class names attached for styling.
+ def merge_request_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project && merge_request = project.merge_requests.find_by(iid: id)
+ push_result(:merge_request, merge_request)
+
+ title = escape_once("Merge Request: #{merge_request.title}")
+ klass = reference_class(:merge_request)
+
+ url = url_for_merge_request(merge_request, project)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}!#{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_merge_request(mr, project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_merge_request_url(project.namespace, project, mr,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/reference_filter.rb b/lib/gitlab/markdown/reference_filter.rb
new file mode 100644
index 00000000000..a4303d96bef
--- /dev/null
+++ b/lib/gitlab/markdown/reference_filter.rb
@@ -0,0 +1,94 @@
+require 'active_support/core_ext/string/output_safety'
+require 'html/pipeline'
+
+module Gitlab
+ module Markdown
+ # Base class for GitLab Flavored Markdown reference filters.
+ #
+ # References within <pre>, <code>, <a>, and <style> elements are ignored.
+ #
+ # Context options:
+ # :project (required) - Current project, ignored if reference is cross-project.
+ # :reference_class - Custom CSS class added to reference links.
+ # :only_path - Generate path-only links.
+ #
+ # Results:
+ # :references - A Hash of references that were found and replaced.
+ class ReferenceFilter < HTML::Pipeline::Filter
+ def initialize(*args)
+ super
+
+ result[:references] = Hash.new { |hash, type| hash[type] = [] }
+ end
+
+ def escape_once(html)
+ ERB::Util.html_escape_once(html)
+ end
+
+ # Don't look for references in text nodes that are children of these
+ # elements.
+ IGNORE_PARENTS = %w(pre code a style).to_set
+
+ def ignored_ancestry?(node)
+ has_ancestor?(node, IGNORE_PARENTS)
+ end
+
+ def project
+ context[:project]
+ end
+
+ # Add a reference to the pipeline's result Hash
+ #
+ # type - Singular Symbol reference type (e.g., :issue, :user, etc.)
+ # values - One or more Objects to add
+ def push_result(type, *values)
+ return if values.empty?
+
+ result[:references][type].push(*values)
+ end
+
+ def reference_class(type)
+ "gfm gfm-#{type} #{context[:reference_class]}".strip
+ end
+
+ # Iterate through the document's text nodes, yielding the current node's
+ # content if:
+ #
+ # * The `project` context value is present AND
+ # * The node's content matches `pattern` AND
+ # * The node is not an ancestor of an ignored node type
+ #
+ # pattern - Regex pattern against which to match the node's content
+ #
+ # Yields the current node's String contents. The result of the block will
+ # replace the node's existing content and update the current document.
+ #
+ # Returns the updated Nokogiri::XML::Document object.
+ def replace_text_nodes_matching(pattern)
+ return doc if project.nil?
+
+ search_text_nodes(doc).each do |node|
+ content = node.to_html
+
+ next unless content.match(pattern)
+ next if ignored_ancestry?(node)
+
+ html = yield content
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ # Ensure that a :project key exists in context
+ #
+ # Note that while the key might exist, its value could be nil!
+ def validate
+ needs :project
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb
new file mode 100644
index 00000000000..9a154e0b2fe
--- /dev/null
+++ b/lib/gitlab/markdown/sanitization_filter.rb
@@ -0,0 +1,38 @@
+require 'html/pipeline/filter'
+require 'html/pipeline/sanitization_filter'
+
+module Gitlab
+ module Markdown
+ # Sanitize HTML
+ #
+ # Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
+ class SanitizationFilter < HTML::Pipeline::SanitizationFilter
+ def whitelist
+ whitelist = HTML::Pipeline::SanitizationFilter::WHITELIST
+
+ # Allow `class` and `id` on all elements
+ whitelist[:attributes][:all].push('class', 'id')
+
+ # Allow table alignment
+ whitelist[:attributes]['th'] = %w(style)
+ whitelist[:attributes]['td'] = %w(style)
+
+ # Allow span elements
+ whitelist[:elements].push('span')
+
+ # Remove `rel` attribute from `a` elements
+ whitelist[:transformers].push(remove_rel)
+
+ whitelist
+ end
+
+ def remove_rel
+ lambda do |env|
+ if env[:node_name] == 'a'
+ env[:node].remove_attribute('rel')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
new file mode 100644
index 00000000000..64a0a2696f7
--- /dev/null
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -0,0 +1,74 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces snippet references with links. References to
+ # snippets that do not exist are ignored.
+ #
+ # This filter supports cross-project references.
+ class SnippetReferenceFilter < ReferenceFilter
+ include CrossProjectReference
+
+ # Public: Find `$123` snippet references in text
+ #
+ # SnippetReferenceFilter.references_in(text) do |match, snippet|
+ # "<a href=...>$#{snippet}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, the Integer snippet ID, and an optional String
+ # of the external project reference.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(SNIPPET_PATTERN) do |match|
+ yield match, $~[:snippet].to_i, $~[:project]
+ end
+ end
+
+ # Pattern used to extract `$123` snippet references from text
+ #
+ # This pattern supports cross-project references.
+ SNIPPET_PATTERN = /#{PROJECT_PATTERN}?\$(?<snippet>\d+)/
+
+ def call
+ replace_text_nodes_matching(SNIPPET_PATTERN) do |content|
+ snippet_link_filter(content)
+ end
+ end
+
+ # Replace `$123` snippet references in text with links to the referenced
+ # snippets's details page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `$123` references replaced with links. All links
+ # have `gfm` and `gfm-snippet` class names attached for styling.
+ def snippet_link_filter(text)
+ self.class.references_in(text) do |match, id, project_ref|
+ project = self.project_from_ref(project_ref)
+
+ if project && snippet = project.snippets.find_by(id: id)
+ push_result(:snippet, snippet)
+
+ title = escape_once("Snippet: #{snippet.title}")
+ klass = reference_class(:snippet)
+
+ url = url_for_snippet(snippet, project)
+
+ %(<a href="#{url}"
+ title="#{title}"
+ class="#{klass}">#{project_ref}$#{id}</a>)
+ else
+ match
+ end
+ end
+ end
+
+ def url_for_snippet(snippet, project)
+ h = Rails.application.routes.url_helpers
+ h.namespace_project_snippet_url(project.namespace, project, snippet,
+ only_path: context[:only_path])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/table_of_contents_filter.rb b/lib/gitlab/markdown/table_of_contents_filter.rb
new file mode 100644
index 00000000000..38887c9778c
--- /dev/null
+++ b/lib/gitlab/markdown/table_of_contents_filter.rb
@@ -0,0 +1,62 @@
+require 'html/pipeline/filter'
+
+module Gitlab
+ module Markdown
+ # HTML filter that adds an anchor child element to all Headers in a
+ # document, so that they can be linked to.
+ #
+ # Generates the Table of Contents with links to each header. See Results.
+ #
+ # Based on HTML::Pipeline::TableOfContentsFilter.
+ #
+ # Context options:
+ # :no_header_anchors - Skips all processing done by this filter.
+ #
+ # Results:
+ # :toc - String containing Table of Contents data as a `ul` element with
+ # `li` child elements.
+ class TableOfContentsFilter < HTML::Pipeline::Filter
+ PUNCTUATION_REGEXP = /[^\p{Word}\- ]/u
+
+ def call
+ return doc if context[:no_header_anchors]
+
+ result[:toc] = ""
+
+ headers = Hash.new(0)
+
+ doc.css('h1, h2, h3, h4, h5, h6').each do |node|
+ text = node.text
+
+ id = text.downcase
+ id.gsub!(PUNCTUATION_REGEXP, '') # remove punctuation
+ id.gsub!(' ', '-') # replace spaces with dash
+ id.squeeze!('-') # replace multiple dashes with one
+
+ uniq = (headers[id] > 0) ? "-#{headers[id]}" : ''
+ headers[id] += 1
+
+ if header_content = node.children.first
+ href = "#{id}#{uniq}"
+ push_toc(href, text)
+ header_content.add_previous_sibling(anchor_tag(href))
+ end
+ end
+
+ result[:toc] = %Q{<ul class="section-nav">\n#{result[:toc]}</ul>} unless result[:toc].empty?
+
+ doc
+ end
+
+ private
+
+ def anchor_tag(href)
+ %Q{<a id="#{href}" class="anchor" href="##{href}" aria-hidden="true"></a>}
+ end
+
+ def push_toc(href, text)
+ result[:toc] << %Q{<li><a href="##{href}">#{text}</a></li>\n}
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
new file mode 100644
index 00000000000..28ec041b1d4
--- /dev/null
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -0,0 +1,105 @@
+module Gitlab
+ module Markdown
+ # HTML filter that replaces user or group references with links.
+ #
+ # A special `@all` reference is also supported.
+ class UserReferenceFilter < ReferenceFilter
+ # Public: Find `@user` user references in text
+ #
+ # UserReferenceFilter.references_in(text) do |match, username|
+ # "<a href=...>@#{user}</a>"
+ # end
+ #
+ # text - String text to search.
+ #
+ # Yields the String match, and the String user name.
+ #
+ # Returns a String replaced with the return of the block.
+ def self.references_in(text)
+ text.gsub(USER_PATTERN) do |match|
+ yield match, $~[:user]
+ end
+ end
+
+ # Pattern used to extract `@user` user references from text
+ USER_PATTERN = /@(?<user>#{Gitlab::Regex::NAMESPACE_REGEX_STR})/
+
+ def call
+ replace_text_nodes_matching(USER_PATTERN) do |content|
+ user_link_filter(content)
+ end
+ end
+
+ # Replace `@user` user references in text with links to the referenced
+ # user's profile page.
+ #
+ # text - String text to replace references in.
+ #
+ # Returns a String with `@user` references replaced with links. All links
+ # have `gfm` and `gfm-project_member` class names attached for styling.
+ def user_link_filter(text)
+ self.class.references_in(text) do |match, username|
+ if username == 'all'
+ link_to_all
+ elsif namespace = Namespace.find_by(path: username)
+ link_to_namespace(namespace) || match
+ else
+ match
+ end
+ end
+ end
+
+ private
+
+ def urls
+ Rails.application.routes.url_helpers
+ end
+
+ def link_class
+ reference_class(:project_member)
+ end
+
+ def link_to_all
+ project = context[:project]
+
+ # FIXME (rspeicher): Law of Demeter
+ push_result(:user, *project.team.members.flatten)
+
+ url = urls.namespace_project_url(project.namespace, project,
+ only_path: context[:only_path])
+
+ %(<a href="#{url}" class="#{link_class}">@all</a>)
+ end
+
+ def link_to_namespace(namespace)
+ if namespace.is_a?(Group)
+ link_to_group(namespace.path, namespace)
+ else
+ link_to_user(namespace.path, namespace)
+ end
+ end
+
+ def link_to_group(group, namespace)
+ return unless user_can_reference_group?(namespace)
+
+ push_result(:user, *namespace.users)
+
+ url = urls.group_url(group, only_path: context[:only_path])
+
+ %(<a href="#{url}" class="#{link_class}">@#{group}</a>)
+ end
+
+ def link_to_user(user, namespace)
+ push_result(:user, namespace.owner)
+
+ url = urls.user_url(user, only_path: context[:only_path])
+
+ %(<a href="#{url}" class="#{link_class}">@#{user}</a>)
+ end
+
+ def user_can_reference_group?(group)
+ Ability.abilities.allowed?(context[:current_user], :read_group, group)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/middleware/timeout.rb b/lib/gitlab/middleware/timeout.rb
deleted file mode 100644
index 015600392b9..00000000000
--- a/lib/gitlab/middleware/timeout.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module Gitlab
- module Middleware
- class Timeout < Rack::Timeout
- GRACK_REGEX = /[-\/\w\.]+\.git\//.freeze
-
- def call(env)
- return @app.call(env) if env['PATH_INFO'] =~ GRACK_REGEX
-
- super
- end
- end
- end
-end
diff --git a/lib/gitlab/note_data_builder.rb b/lib/gitlab/note_data_builder.rb
index 644dec45dca..ea6b0ee796d 100644
--- a/lib/gitlab/note_data_builder.rb
+++ b/lib/gitlab/note_data_builder.rb
@@ -69,8 +69,8 @@ module Gitlab
def build_data_for_commit(project, user, note)
# commit_id is the SHA hash
- commit = project.repository.commit(note.commit_id)
- commit.hook_attrs(project)
+ commit = project.commit(note.commit_id)
+ commit.hook_attrs
end
end
end
diff --git a/lib/gitlab/oauth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index ce52beec78e..0f16c925900 100644
--- a/lib/gitlab/oauth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -9,11 +9,11 @@ module Gitlab
end
def uid
- auth_hash.uid.to_s
+ Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
end
def provider
- auth_hash.provider
+ Gitlab::Utils.force_utf8(auth_hash.provider.to_s)
end
def info
@@ -21,23 +21,28 @@ module Gitlab
end
def name
- (info.try(:name) || full_name).to_s.force_encoding('utf-8')
+ Gitlab::Utils.force_utf8((info.try(:name) || full_name).to_s)
end
def full_name
- "#{info.first_name} #{info.last_name}"
+ Gitlab::Utils.force_utf8("#{info.first_name} #{info.last_name}")
end
def username
- (info.try(:nickname) || generate_username).to_s.force_encoding('utf-8')
+ Gitlab::Utils.force_utf8(
+ (info.try(:nickname) || generate_username).to_s
+ )
end
def email
- (info.try(:email) || generate_temporarily_email).downcase
+ Gitlab::Utils.force_utf8(
+ (info.try(:email) || generate_temporarily_email).downcase
+ )
end
def password
- @password ||= Devise.friendly_token[0, 8].downcase
+ devise_friendly_token = Devise.friendly_token[0, 8].downcase
+ @password ||= Gitlab::Utils.force_utf8(devise_friendly_token)
end
# Get the first part of the email address (before @)
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/popen.rb b/lib/gitlab/popen.rb
index fea4d2d55d2..43e07e09160 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -29,7 +29,7 @@ module Gitlab
@cmd_status = wait_thr.value.exitstatus
end
- return @cmd_output, @cmd_status
+ [@cmd_output, @cmd_status]
end
end
end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 8b85f3da83f..0dab7bcfa4d 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -67,7 +67,7 @@ module Gitlab
end
def notes
- Note.where(project_id: limit_project_ids).search(query).order('updated_at DESC')
+ Note.where(project_id: limit_project_ids).user.search(query).order('updated_at DESC')
end
def limit_project_ids
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
index 5cefa67d3ab..f97784f5abb 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/push_data_builder.rb
@@ -21,20 +21,26 @@ module Gitlab
# total_commits_count: Fixnum
# }
#
- def build(project, user, oldrev, newrev, ref, commits = [])
+ def build(project, user, oldrev, newrev, ref, commits = [], message = nil)
# Total commits count
commits_count = commits.size
# Get latest 20 commits ASC
commits_limited = commits.last(20)
+
+ # For performance purposes maximum 20 latest commits
+ # will be passed as post receive hook data.
+ commit_attrs = commits_limited.map(&:hook_attrs)
+ type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push"
# Hash to be passed as post_receive_data
data = {
- object_kind: "push",
+ object_kind: type,
before: oldrev,
after: newrev,
ref: ref,
checkout_sha: checkout_sha(project.repository, newrev, ref),
+ message: message,
user_id: user.id,
user_name: user.name,
user_email: user.email,
@@ -48,16 +54,10 @@ module Gitlab
git_ssh_url: project.ssh_url_to_repo,
visibility_level: project.visibility_level
},
- commits: [],
+ commits: commit_attrs,
total_commits_count: commits_count
}
- # For performance purposes maximum 20 latest commits
- # will be passed as post receive hook data.
- commits_limited.each do |commit|
- data[:commits] << commit.hook_attrs(project)
- end
-
data
end
@@ -65,12 +65,14 @@ module Gitlab
# existing project and commits to test web hooks
def build_sample(project, user)
commits = project.repository.commits(project.default_branch, nil, 3)
- build(project, user, commits.last.id, commits.first.id, "refs/heads/#{project.default_branch}", commits)
+ ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}#{project.default_branch}"
+ build(project, user, commits.last.id, commits.first.id, ref, commits)
end
def checkout_sha(repository, newrev, ref)
- if newrev != Gitlab::Git::BLANK_SHA && ref.start_with?('refs/tags/')
- tag_name = Gitlab::Git.extract_ref_name(ref)
+ # Find sha for tag, except when it was deleted.
+ if Gitlab::Git.tag_ref?(ref) && !Gitlab::Git.blank_ref?(newrev)
+ tag_name = Gitlab::Git.ref_name(ref)
tag = repository.find_tag(tag_name)
if tag
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 5b9772de168..e35f848fa6e 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -1,88 +1,77 @@
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
-
- 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)
- parse_references(string.dup, project)
+ def analyze(text)
+ @_text = text.dup
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
+ result = pipeline_result(:user)
+ result.uniq
end
- def labels_for(project = nil)
- labels.map do |entry|
- project.labels.where(id: entry[:id]).first
- end.reject(&:nil?)
+ def labels
+ result = pipeline_result(:label)
+ result.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
- end
- end.reject(&:nil?)
+ def issues
+ # TODO (rspeicher): What about external issues?
+
+ result = pipeline_result(:issue)
+ result.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
+ result = pipeline_result(:merge_request)
+ result.uniq
end
- def snippets_for(project)
- snippets.map do |entry|
- project.snippets.where(id: entry[:id]).first
- end.reject(&:nil?)
+ def snippets
+ result = pipeline_result(:snippet)
+ result.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
+ result = pipeline_result(:commit)
+ result.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)
- [repo.commit(from_id), repo.commit(to_id)]
- end
- end.reject(&:nil?)
+ def commit_ranges
+ result = pipeline_result(:commit_range)
+ result.uniq
end
private
- def reference_link(type, identifier, project, _)
- # Append identifier to the appropriate collection.
- send("#{type}s") << { project: project, id: identifier }
- end
+ # Instantiate and call HTML::Pipeline with a single reference filter type,
+ # returning the result
+ #
+ # filter_type - Symbol reference type (e.g., :commit, :issue, etc.)
+ #
+ # Returns the results Array for the requested filter type
+ def pipeline_result(filter_type)
+ klass = filter_type.to_s.camelize + 'ReferenceFilter'
+ filter = "Gitlab::Markdown::#{klass}".constantize
+
+ context = {
+ project: project,
+ current_user: current_user,
+ # We don't actually care about the links generated
+ only_path: true
+ }
+
+ pipeline = HTML::Pipeline.new([filter], context)
+ result = pipeline.call(@_text)
- def should_lookup?(project, entry_project)
- if entry_project.nil?
- false
- else
- project.nil? || entry_project.default_issues_tracker?
- end
+ result[:references][filter_type]
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index cf6e260f257..9f1adc860d1 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[\p{Alnum}\p{Pd}_\. ]*\z/.freeze
end
- def username_regex_message
- default_regex_message
+ def namespace_name_regex_message
+ "can contain only letters, digits, '_', '.', dash and space."
end
+
def project_name_regex
- /\A[a-zA-Z0-9_.][a-zA-Z0-9_\-\. ]*\z/
+ @project_name_regex ||= /\A[\p{Alnum}_][\p{Alnum}\p{Pd}_\. ]*\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, '_', '.', dash 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/merge_action.rb b/lib/gitlab/satellite/merge_action.rb
index 25122666f5e..1f2e5f82dd5 100644
--- a/lib/gitlab/satellite/merge_action.rb
+++ b/lib/gitlab/satellite/merge_action.rb
@@ -97,7 +97,7 @@ module Gitlab
in_locked_and_timed_satellite do |merge_repo|
prepare_satellite!(merge_repo)
update_satellite_source_and_target!(merge_repo)
- if (merge_request.for_fork?)
+ if merge_request.for_fork?
repository = Gitlab::Git::Repository.new(merge_repo.path)
commits = Gitlab::Git::Commit.between(
repository,
diff --git a/lib/gitlab/satellite/satellite.rb b/lib/gitlab/satellite/satellite.rb
index 70125d539da..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
@@ -99,11 +104,7 @@ module Gitlab
heads = repo.heads.map(&:name)
# update or create the parking branch
- if heads.include? PARKING_BRANCH
- repo.git.checkout({}, PARKING_BRANCH)
- else
- repo.git.checkout(default_options({ b: true }), PARKING_BRANCH)
- end
+ repo.git.checkout(default_options({ B: true }), PARKING_BRANCH)
# remove the parking branch from the list of heads ...
heads.delete(PARKING_BRANCH)
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
index 0f2db50e98c..f33b2dedf4a 100644
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ b/lib/gitlab/sidekiq_middleware/memory_killer.rb
@@ -7,6 +7,7 @@ module Gitlab
GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
# Wait 30 seconds for running jobs to finish during graceful shutdown
SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
+ SHUTDOWN_SIGNAL = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL'] || 'SIGTERM').to_s
# Create a mutex used to ensure there will be only one thread waiting to
# shut Sidekiq down
@@ -24,19 +25,19 @@ module Gitlab
Sidekiq.logger.warn "current RSS #{current_rss} exceeds maximum RSS "\
"#{MAX_RSS}"
- Sidekiq.logger.warn "spawned thread that will shut down PID "\
- "#{Process.pid} in #{GRACE_TIME} seconds"
+ Sidekiq.logger.warn "this thread will shut down PID #{Process.pid} "\
+ "in #{GRACE_TIME} seconds"
sleep(GRACE_TIME)
Sidekiq.logger.warn "sending SIGUSR1 to PID #{Process.pid}"
Process.kill('SIGUSR1', Process.pid)
Sidekiq.logger.warn "waiting #{SHUTDOWN_WAIT} seconds before sending "\
- "SIGTERM to PID #{Process.pid}"
+ "#{SHUTDOWN_SIGNAL} to PID #{Process.pid}"
sleep(SHUTDOWN_WAIT)
- Sidekiq.logger.warn "sending SIGTERM to PID #{Process.pid}"
- Process.kill('SIGTERM', Process.pid)
+ Sidekiq.logger.warn "sending #{SHUTDOWN_SIGNAL} to PID #{Process.pid}"
+ Process.kill(SHUTDOWN_SIGNAL, Process.pid)
end
end
diff --git a/lib/gitlab/theme.rb b/lib/gitlab/theme.rb
index a7c83a880f6..e5a1f1b44d9 100644
--- a/lib/gitlab/theme.rb
+++ b/lib/gitlab/theme.rb
@@ -5,33 +5,46 @@ module Gitlab
MODERN = 3 unless const_defined?(:MODERN)
GRAY = 4 unless const_defined?(:GRAY)
COLOR = 5 unless const_defined?(:COLOR)
+ BLUE = 6 unless const_defined?(:BLUE)
- def self.css_class_by_id(id)
- themes = {
- BASIC => "ui_basic",
- MARS => "ui_mars",
- MODERN => "ui_modern",
- GRAY => "ui_gray",
- COLOR => "ui_color"
+ def self.classes
+ @classes ||= {
+ BASIC => 'ui_basic',
+ MARS => 'ui_mars',
+ MODERN => 'ui_modern',
+ GRAY => 'ui_gray',
+ COLOR => 'ui_color',
+ BLUE => 'ui_blue'
}
+ end
+ def self.css_class_by_id(id)
id ||= Gitlab.config.gitlab.default_theme
-
- return themes[id]
+ classes[id]
end
- def self.type_css_class_by_id(id)
- types = {
+ def self.types
+ @types ||= {
BASIC => 'light_theme',
MARS => 'dark_theme',
MODERN => 'dark_theme',
GRAY => 'dark_theme',
- COLOR => 'dark_theme'
+ COLOR => 'dark_theme',
+ BLUE => 'light_theme'
}
+ end
+ def self.type_css_class_by_id(id)
id ||= Gitlab.config.gitlab.default_theme
-
types[id]
end
+
+ # Convenience method to get a space-separated String of all the theme
+ # classes that might be applied to the `body` element
+ #
+ # Returns a String
+ def self.body_classes
+ (classes.values + types.values).uniq.join(' ')
+ end
end
end
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 6830d15875a..11b0d44f340 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -51,9 +51,9 @@ module Gitlab
anchor: "note_#{note.id}")
elsif note.for_project_snippet?
snippet = Snippet.find(note.noteable_id)
- snippet_url(snippet,
- host: Gitlab.config.gitlab['url'],
- anchor: "note_#{note.id}")
+ project_snippet_url(snippet,
+ host: Gitlab.config.gitlab['url'],
+ anchor: "note_#{note.id}")
end
end
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index bd184c27187..d13fe0ef8a9 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -9,5 +9,9 @@ module Gitlab
def system_silent(cmd)
Popen::popen(cmd).last.zero?
end
+
+ def force_utf8(str)
+ str.force_encoding(Encoding::UTF_8)
+ end
end
end
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index d0b6cde3c7e..582fc759efd 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -5,6 +5,8 @@
#
module Gitlab
module VisibilityLevel
+ extend CurrentSettings
+
PRIVATE = 0 unless const_defined?(:PRIVATE)
INTERNAL = 10 unless const_defined?(:INTERNAL)
PUBLIC = 20 unless const_defined?(:PUBLIC)
@@ -23,21 +25,27 @@ module Gitlab
end
def allowed_for?(user, level)
- user.is_admin? || allowed_level?(level)
+ user.is_admin? || allowed_level?(level.to_i)
end
- # Level can be a string `"public"` or a value `20`, first check if valid,
- # then check if the corresponding string appears in the config
+ # Return true if the specified level is allowed for the current user.
+ # Level should be a numeric value, e.g. `20`.
def allowed_level?(level)
- if options.has_key?(level.to_s)
- non_restricted_level?(level)
- elsif options.has_value?(level.to_i)
- non_restricted_level?(options.key(level.to_i).downcase)
- end
+ valid_level?(level) && non_restricted_level?(level)
end
def non_restricted_level?(level)
- ! Gitlab.config.gitlab.restricted_visibility_levels.include?(level)
+ restricted_levels = current_application_settings.restricted_visibility_levels
+
+ if restricted_levels.nil?
+ true
+ else
+ !restricted_levels.include?(level)
+ end
+ end
+
+ def valid_level?(level)
+ options.has_value?(level)
end
end
diff --git a/lib/redcarpet/render/gitlab_html.rb b/lib/redcarpet/render/gitlab_html.rb
index 714261f815c..bea66e6cdc1 100644
--- a/lib/redcarpet/render/gitlab_html.rb
+++ b/lib/redcarpet/render/gitlab_html.rb
@@ -1,24 +1,20 @@
-class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
+require 'active_support/core_ext/string/output_safety'
+class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
attr_reader :template
alias_method :h, :template
- def initialize(template, options = {})
+ def initialize(template, color_scheme, options = {})
@template = template
+ @color_scheme = color_scheme
@project = @template.instance_variable_get("@project")
@options = options.dup
- super options
+
+ super(options)
end
- # If project has issue number 39, apostrophe will be linked in
- # regular text to the issue as Redcarpet will convert apostrophe to
- # #39;
- # We replace apostrophe with right single quote before Redcarpet
- # does the processing and put the apostrophe back in postprocessing.
- # This only influences regular text, code blocks are untouched.
def normal_text(text)
- return text unless text.present?
- text.gsub("'", "&rsquo;")
+ ERB::Util.html_escape_once(text)
end
# Stolen from Rugments::Plugins::Redcarpet as this module is not required
@@ -30,38 +26,20 @@ class Redcarpet::Render::GitlabHTML < Redcarpet::Render::HTML
# so we assume you're not using leading spaces that aren't tabs,
# and just replace them here.
if lexer.tag == 'make'
- code.gsub! /^ /, "\t"
+ code.gsub!(/^ /, "\t")
end
formatter = Rugments::Formatters::HTML.new(
- cssclass: "code highlight white #{lexer.tag}"
+ cssclass: "code highlight #{@color_scheme} #{lexer.tag}"
)
formatter.format(lexer.lex(code))
end
- def link(link, title, content)
- h.link_to_gfm(content, link, title: title)
- end
-
- def header(text, level)
- if @options[:no_header_anchors]
- "<h#{level}>#{text}</h#{level}>"
- else
- id = ActionController::Base.helpers.strip_tags(h.gfm(text)).downcase() \
- .gsub(/[^a-z0-9_-]/, '-').gsub(/-+/, '-').gsub(/^-/, '').gsub(/-$/, '')
- "<h#{level} id=\"#{id}\">#{text}<a href=\"\##{id}\"></a></h#{level}>"
- end
- end
-
def postprocess(full_document)
- full_document.gsub!("&rsquo;", "'")
unless @template.instance_variable_get("@project_wiki") || @project.nil?
full_document = h.create_relative_links(full_document)
end
- if @options[:parse_tasks]
- h.gfm_with_tasks(full_document)
- else
- h.gfm(full_document)
- end
+
+ h.gfm_with_options(full_document, @options)
end
end
diff --git a/lib/support/deploy/deploy.sh b/lib/support/deploy/deploy.sh
index 4684957233a..adea4c7a747 100755
--- a/lib/support/deploy/deploy.sh
+++ b/lib/support/deploy/deploy.sh
@@ -4,7 +4,7 @@
# If any command return non-zero status - stop deploy
set -e
-echo 'Deploy: Stoping sidekiq..'
+echo 'Deploy: Stopping sidekiq..'
cd /home/git/gitlab/ && sudo -u git -H bundle exec rake sidekiq:stop RAILS_ENV=production
echo 'Deploy: Show deploy index page'
diff --git a/lib/tasks/brakeman.rake b/lib/tasks/brakeman.rake
index abcb5f0ae46..3a225801ff2 100644
--- a/lib/tasks/brakeman.rake
+++ b/lib/tasks/brakeman.rake
@@ -1,7 +1,7 @@
desc 'Security check via brakeman'
task :brakeman do
if system("brakeman --skip-files lib/backup/repository.rb -w3 -z")
- exit 0
+ puts 'Security check succeed'
else
puts 'Security check failed'
exit 1
diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake
index 058c7417040..b22c631c8ba 100644
--- a/lib/tasks/dev.rake
+++ b/lib/tasks/dev.rake
@@ -7,4 +7,9 @@ namespace :dev do
Rake::Task["gitlab:setup"].invoke
Rake::Task["gitlab:shell:setup"].invoke
end
+
+ desc 'GITLAB | Start/restart foreman and watch for changes'
+ task :foreman => :environment do
+ sh 'rerun --dir app,config,lib -- foreman start'
+ end
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 43115915de1..1a6303b6c82 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -29,6 +29,7 @@ namespace :gitlab do
check_redis_version
check_ruby_version
check_git_version
+ check_active_users
finished_checking "GitLab"
end
@@ -281,7 +282,8 @@ namespace :gitlab do
def check_redis_version
print "Redis version >= 2.0.0? ... "
- if run_and_match(%W(redis-cli --version), /redis-cli 2.\d.\d/)
+ redis_version = run(%W(redis-cli --version))
+ if redis_version.try(:match, /redis-cli 2.\d.\d/) || redis_version.try(:match, /redis-cli 3.\d.\d/)
puts "yes".green
else
puts "no".red
@@ -328,16 +330,20 @@ namespace :gitlab do
if correct_options.all?
puts "yes".green
else
- puts "no".red
- try_fixing_it(
- sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""),
- sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""),
- sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"")
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
+ print "Trying to fix Git error automatically. ..."
+ if auto_fix_git_config(options)
+ puts "Success".green
+ else
+ puts "Failed".red
+ try_fixing_it(
+ sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.name \"#{options["user.name"]}\""),
+ sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global user.email \"#{options["user.email"]}\""),
+ sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"")
+ )
+ for_more_information(
+ see_installation_guide_section "GitLab"
+ )
+ end
end
end
end
@@ -682,6 +688,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
##########################
@@ -781,6 +804,10 @@ namespace :gitlab do
end
end
+ def check_active_users
+ puts "Active users: #{User.active.count}"
+ end
+
def omnibus_gitlab?
Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails'
end
@@ -801,3 +828,4 @@ namespace :gitlab do
end
end
end
+
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 189ad6090a4..3c9802a0be4 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -90,13 +90,14 @@ namespace :gitlab do
warn_user_is_not_gitlab
block_flag = ENV['BLOCK']
- User.ldap.each do |ldap_user|
- print "#{ldap_user.name} (#{ldap_user.extern_uid}) ..."
- if Gitlab::LDAP::Access.allowed?(ldap_user)
+ User.find_each do |user|
+ next unless user.ldap_user?
+ print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
+ if Gitlab::LDAP::Access.allowed?(user)
puts " [OK]".green
else
if block_flag
- ldap_user.block! unless ldap_user.blocked?
+ user.block! unless user.blocked?
puts " [BLOCKED]".red
else
puts " [NOT IN LDAP]".yellow
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index 9af93300e08..e835d6cb9b7 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -112,6 +112,7 @@ namespace :gitlab do
print '.'
end
end
+ puts ""
unless $?.success?
puts "Failed to add keys...".red
diff --git a/lib/tasks/gitlab/task_helpers.rake b/lib/tasks/gitlab/task_helpers.rake
index da61c6e007f..14a130be2ca 100644
--- a/lib/tasks/gitlab/task_helpers.rake
+++ b/lib/tasks/gitlab/task_helpers.rake
@@ -112,4 +112,20 @@ namespace :gitlab do
@warned_user_not_gitlab = true
end
end
+
+ # Tries to configure git itself
+ #
+ # Returns true if all subcommands were successfull (according to their exit code)
+ # Returns false if any or all subcommands failed.
+ def auto_fix_git_config(options)
+ if !@warned_user_not_gitlab && options['user.email'] != 'example@example.com' # default email should be overridden?
+ command_success = options.map do |name, value|
+ system(%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
+ end
+
+ command_success.all?
+ else
+ false
+ end
+ end
end
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/lib/tasks/jasmine.rake b/lib/tasks/jasmine.rake
new file mode 100644
index 00000000000..9e2cceffa19
--- /dev/null
+++ b/lib/tasks/jasmine.rake
@@ -0,0 +1,12 @@
+# Since we no longer explicitly require the 'jasmine' gem, we lost the
+# `jasmine:ci` task used by GitLab CI jobs.
+#
+# This provides a simple alias to run the `spec:javascript` task from the
+# 'jasmine-rails' gem.
+task jasmine: ['jasmine:ci']
+
+namespace :jasmine do
+ task :ci do
+ Rake::Task['spec:javascript'].invoke
+ end
+end
diff --git a/public/503.html b/public/503.html
deleted file mode 100644
index efdae0f512d..00000000000
--- a/public/503.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>Page took too long to load (503)</title>
- <link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
-</head>
-<body>
- <h1>503</h1>
- <h3>Page took too long to load.</h3>
- <hr/>
- <p>Please contact your GitLab administrator if this problem persists.</p>
-</body>
-</html>
diff --git a/public/deploy.html b/public/deploy.html
index d9c4bb5c583..e41ed76573d 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html>
<head>
- <title>Deploy in progress. Please try again in few minutes</title>
+ <title>Deploy in progress. Please try again in a few minutes</title>
<link href="/static.css" media="screen" rel="stylesheet" type="text/css" />
</head>
<body>
<h1><center><img src="/gitlab_logo.png"/></center>Deploy in progress</h1>
- <h3>Please try again in few minutes or contact your administrator.</h3>
+ <h3>Please try again in a few minutes or contact your administrator.</h3>
</body>
</html>
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/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index 3394a1f863f..2cfa399a047 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe Projects::CommitController do
let(:project) { create(:project) }
let(:user) { create(:user) }
- let(:commit) { project.repository.commit("master") }
+ let(:commit) { project.commit("master") }
before do
sign_in(user)
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 b8820413406..3d3846b2e3a 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -27,17 +27,20 @@ describe Import::GithubController do
describe "GET status" do
before do
@repo = OpenStruct.new(login: 'vim', full_name: 'asd/vim')
+ @org = OpenStruct.new(login: 'company')
+ @org_repo = OpenStruct.new(login: 'company', full_name: 'company/repo')
end
it "assigns variables" do
@project = create(:project, import_type: 'github', creator_id: user.id)
controller.stub_chain(:client, :repos).and_return([@repo])
- controller.stub_chain(:client, :orgs).and_return([])
+ controller.stub_chain(:client, :orgs).and_return([@org])
+ controller.stub_chain(:client, :org_repos).with(@org.login).and_return([@org_repo])
get :status
expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([@repo])
+ expect(assigns(:repos)).to eq([@repo, @org_repo])
end
it "does not show already added project" do
@@ -53,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
- 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 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
+
+ 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..78c0f5079cc
--- /dev/null
+++ b/spec/controllers/import/google_code_controller_spec.rb
@@ -0,0 +1,60 @@
+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])
+ controller.stub_chain(:client, :incompatible_repos).and_return([])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([@repo])
+ expect(assigns(:incompatible_repos)).to eq([])
+ 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])
+ controller.stub_chain(:client, :incompatible_repos).and_return([])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([])
+ end
+
+ it "does not show any invalid projects" do
+ controller.stub_chain(:client, :repos).and_return([])
+ controller.stub_chain(:client, :incompatible_repos).and_return([@repo])
+
+ get :status
+
+ expect(assigns(:repos)).to be_empty
+ expect(assigns(:incompatible_repos)).to eq([@repo])
+ end
+ end
+end
diff --git a/spec/controllers/merge_requests_controller_spec.rb b/spec/controllers/merge_requests_controller_spec.rb
index d6f56ed33d6..c94ef1629ae 100644
--- a/spec/controllers/merge_requests_controller_spec.rb
+++ b/spec/controllers/merge_requests_controller_spec.rb
@@ -78,4 +78,24 @@ describe Projects::MergeRequestsController do
end
end
end
+
+ context '#diffs with forked projects with submodules' do
+ render_views
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:forked_project_with_submodules) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) }
+
+ before do
+ fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ fork_project.save
+ merge_request.reload
+ end
+
+ it '#diffs' do
+ get(:diffs, namespace_id: project.namespace.to_param,
+ project_id: project.to_param, id: merge_request.iid, format: 'json')
+ expect(response).to be_success
+ expect(response.body).to have_content('Subproject commit')
+ 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/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
new file mode 100644
index 00000000000..23e1566b8f3
--- /dev/null
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Projects::CompareController do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:ref_from) { "improve%2Fawesome" }
+ let(:ref_to) { "feature" }
+
+ before do
+ sign_in(user)
+ project.team << [user, :master]
+ end
+
+ it 'compare should show some diffs' do
+ get(:show, namespace_id: project.namespace.to_param,
+ project_id: project.to_param, from: ref_from, to: ref_to)
+
+ expect(response).to be_success
+ expect(assigns(:diffs).length).to be >= 1
+ expect(assigns(:commits).length).to be >= 1
+ 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/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb
index 029f48b2d7a..f51abfedae5 100644
--- a/spec/controllers/projects/uploads_controller_spec.rb
+++ b/spec/controllers/projects/uploads_controller_spec.rb
@@ -54,4 +54,227 @@ describe Projects::UploadsController do
end
end
end
+
+ describe "GET #show" do
+ let(:go) do
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ secret: "123456",
+ filename: "image.jpg"
+ end
+
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ context "when not signed in" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ go
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ go
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ go
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ go
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context "when not signed in" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ context "when the file is an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ go
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "redirects to the sign in page" do
+ go
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "redirects to the sign in page" do
+ go
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ 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
+
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ context "when the file is an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ go
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "redirects to the sign in page" do
+ go
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "redirects to the sign in page" do
+ go
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+ end
+
+ context "when the user isn't blocked" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ go
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ go
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ context "when the file exists" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:file).and_return(jpg)
+ allow(jpg).to receive(:exists?).and_return(true)
+ end
+
+ context "when the file is an image" do
+ before do
+ allow_any_instance_of(FileUploader).to receive(:image?).and_return(true)
+ end
+
+ it "responds with status 200" do
+ go
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when the file is not an image" do
+ it "responds with status 404" do
+ go
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ context "when the file doesn't exist" do
+ it "responds with status 404" do
+ go
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
new file mode 100644
index 00000000000..0f9780356b1
--- /dev/null
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -0,0 +1,296 @@
+require 'spec_helper'
+
+describe UploadsController do
+ let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+
+ describe "GET show" do
+ context "when viewing a user avatar" do
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ context "when the user is blocked" do
+ before do
+ user.block
+ end
+
+ it "redirects to the sign in page" do
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "responds with status 200" do
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when not signed in" do
+ it "responds with status 200" do
+ get :show, model: "user", mounted_as: "avatar", id: user.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when viewing a project avatar" do
+ let!(:project) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ context "when not signed in" do
+ it "responds with status 200" do
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ it "responds with status 200" do
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context "when not signed in" do
+ it "redirects to the sign in page" do
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+
+ 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, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "responds with status 200" do
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ it "responds with status 404" do
+ get :show, model: "project", mounted_as: "avatar", id: project.id, filename: "image.png"
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+
+ context "when viewing a group avatar" do
+ let!(:group) { create(:group, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
+ 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 "responds with status 200" do
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ it "responds with status 200" do
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ 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, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+
+ 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, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "responds with status 200" do
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ it "responds with status 404" do
+ get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+
+ context "when viewing a note attachment" do
+ let!(:note) { create(:note, :with_attachment) }
+ let(:project) { note.project }
+
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
+ end
+
+ context "when not signed in" do
+ it "responds with status 200" do
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context "when signed in" do
+ before do
+ sign_in(user)
+ end
+
+ it "responds with status 200" do
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
+
+ context "when not signed in" do
+ it "redirects to the sign in page" do
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+
+ 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, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context "when the user isn't blocked" do
+ it "responds with status 200" do
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+
+ expect(response.status).to eq(200)
+ end
+ end
+ end
+
+ context "when the user doesn't have access to the project" do
+ it "responds with status 404" do
+ get :show, model: "note", mounted_as: "attachment", id: note.id, filename: "image.png"
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 44225c054f2..d47a37914df 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -1,27 +1,46 @@
require 'spec_helper'
describe UsersController do
- let(:user) { create(:user, username: "user1", name: "User 1", email: "user1@gitlab.com") }
+ let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
before do
sign_in(user)
end
- describe "GET #show" do
+ describe 'GET #show' do
render_views
- it "renders the show template" do
+ it 'renders the show template' do
get :show, username: user.username
expect(response.status).to eq(200)
- expect(response).to render_template("show")
+ expect(response).to render_template('show')
end
end
- describe "GET #calendar" do
- it "renders calendar" do
+ describe 'GET #calendar' do
+ it 'renders calendar' do
get :calendar, username: user.username
- expect(response).to render_template("calendar")
+ expect(response).to render_template('calendar')
end
end
-end
+ describe 'GET #calendar_activities' do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user) }
+
+ before do
+ allow_any_instance_of(User).to receive(:contributed_projects_ids).and_return([project.id])
+ project.team << [user, :developer]
+ end
+
+ it 'assigns @calendar_date' do
+ get :calendar_activities, username: user.username, date: '2014-07-31'
+ expect(assigns(:calendar_date)).to eq(Date.parse('2014-07-31'))
+ end
+
+ it 'renders calendar_activities' do
+ get :calendar_activities, username: user.username
+ expect(response).to render_template('calendar_activities')
+ end
+ end
+end
diff --git a/spec/factories.rb b/spec/factories.rb
index fc103e5b133..19f2935f30e 100644
--- a/spec/factories.rb
+++ b/spec/factories.rb
@@ -22,6 +22,7 @@ FactoryGirl.define do
password "12345678"
confirmed_at { Time.now }
confirmation_token { nil }
+ can_create_group true
trait :admin do
admin true
@@ -101,21 +102,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/projects.rb b/spec/factories/projects.rb
index 0899a7603fc..102678a1d74 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -76,6 +76,14 @@ FactoryGirl.define do
end
end
+ factory :forked_project_with_submodules, parent: :empty_project do
+ path { 'forked-gitlabhq' }
+
+ after :create do |project|
+ TestEnv.copy_forked_repo_with_submodules(project)
+ end
+ end
+
factory :redmine_project, parent: :project do
after :create do |project|
project.create_redmine_service(
@@ -86,10 +94,26 @@ FactoryGirl.define do
'new_issue_url' => 'http://redmine/projects/project_name_in_redmine/issues/new'
}
)
- end
- after :create do |project|
+
project.issues_tracker = 'redmine'
project.issues_tracker_id = 'project_name_in_redmine'
end
end
+
+ factory :jira_project, parent: :project do
+ after :create do |project|
+ project.create_jira_service(
+ active: true,
+ properties: {
+ 'title' => 'JIRA tracker',
+ 'project_url' => 'http://jira.example/issues/?jql=project=A',
+ 'issues_url' => 'http://jira.example/browse/:id',
+ 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
+ }
+ )
+
+ project.issues_tracker = 'jira'
+ project.issues_tracker_id = 'project_name_in_jira'
+ end
+ end
end
diff --git a/spec/factories_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/atom/users_spec.rb b/spec/features/atom/users_spec.rb
index c0316b073ad..770ac04c2c5 100644
--- a/spec/features/atom/users_spec.rb
+++ b/spec/features/atom/users_spec.rb
@@ -15,17 +15,24 @@ describe "User Feed", feature: true do
let(:project) { create(:project) }
let(:issue) do
create(:issue, project: project,
- author: user, description: '')
+ author: user, description: "Houston, we have a bug!\n\n***\n\nI guess.")
end
let(:note) do
create(:note, noteable: issue, author: user,
- note: 'Bug confirmed', project: project)
+ note: 'Bug confirmed :+1:', project: project)
+ end
+ let(:merge_request) do
+ create(:merge_request,
+ title: 'Fix bug', author: user,
+ source_project: project, target_project: project,
+ description: "Here is the fix: ![an image](image.png)")
end
before do
project.team << [user, :master]
issue_event(issue, user)
note_event(note, user)
+ merge_request_event(merge_request, user)
visit user_path(user, :atom, private_token: user.private_token)
end
@@ -37,6 +44,18 @@ describe "User Feed", feature: true do
expect(body).
to have_content("#{safe_name} commented on issue ##{issue.iid}")
end
+
+ it 'should have XHTML summaries in issue descriptions' do
+ expect(body).to match /we have a bug!<\/p>\n\n<hr ?\/>\n\n<p>I guess/
+ end
+
+ it 'should have XHTML summaries in notes' do
+ expect(body).to match /Bug confirmed <img[^>]*\/>/
+ end
+
+ it 'should have XHTML summaries in merge request descriptions' do
+ expect(body).to match /Here is the fix: <img[^>]*\/>/
+ end
end
end
@@ -48,6 +67,10 @@ describe "User Feed", feature: true do
EventCreateService.new.leave_note(note, user)
end
+ def merge_request_event(request, user)
+ EventCreateService.new.open_mr(request, user)
+ end
+
def safe_name
html_escape(user.name)
end
diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb
index fca1a06eb88..133beba7b98 100644
--- a/spec/features/gitlab_flavored_markdown_spec.rb
+++ b/spec/features/gitlab_flavored_markdown_spec.rb
@@ -14,7 +14,7 @@ describe "GitLab Flavored Markdown", feature: true do
Commit.any_instance.stub(title: "fix ##{issue.iid}\n\nask @#{fred.username} for details")
end
- let(:commit) { project.repository.commit }
+ let(:commit) { project.commit }
before do
login_as :user
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..66d73b2505c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -21,7 +21,7 @@ describe 'Issues', feature: true do
end
before do
- visit namespace_project_issues_path(project.namespace, project)
+ visit edit_namespace_project_issue_path(project.namespace, project, issue)
click_link "Edit"
end
@@ -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_title: IssuableFinder::NONE)
expect(page).not_to have_content 'foobar'
expect(page).to have_content 'barbaz'
@@ -103,7 +103,7 @@ describe 'Issues', feature: true do
end
it 'should allow filtering by a specified milestone' do
- visit namespace_project_issues_path(project.namespace, project, milestone_id: issue.milestone.id)
+ visit namespace_project_issues_path(project.namespace, project, milestone_title: issue.milestone.title)
expect(page).to have_content 'foobar'
expect(page).not_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/markdown_spec.rb b/spec/features/markdown_spec.rb
new file mode 100644
index 00000000000..1746ce128e4
--- /dev/null
+++ b/spec/features/markdown_spec.rb
@@ -0,0 +1,413 @@
+require 'spec_helper'
+require 'erb'
+
+# This feature spec is intended to be a comprehensive exercising of all of
+# GitLab's non-standard Markdown parsing and the integration thereof.
+#
+# These tests should be very high-level. Anything low-level belongs in the specs
+# for the corresponding HTML::Pipeline filter or helper method.
+#
+# The idea is to pass a Markdown document through our entire processing stack.
+#
+# The process looks like this:
+#
+# Raw Markdown
+# -> `markdown` helper
+# -> Redcarpet::Render::GitlabHTML converts Markdown to HTML
+# -> Post-process HTML
+# -> `gfm_with_options` helper
+# -> HTML::Pipeline
+# -> Sanitize
+# -> Emoji
+# -> Table of Contents
+# -> Autolinks
+# -> Rinku (http, https, ftp)
+# -> Other schemes
+# -> References
+# -> TaskList
+# -> `html_safe`
+# -> Template
+#
+# See the MarkdownFeature class for setup details.
+
+describe 'GitLab Markdown' do
+ include ActionView::Helpers::TagHelper
+ include ActionView::Helpers::UrlHelper
+ include Capybara::Node::Matchers
+ include GitlabMarkdownHelper
+
+ # `markdown` calls these two methods
+ def current_user
+ @feat.user
+ end
+
+ def user_color_scheme_class
+ :white
+ end
+
+ # Let's only parse this thing once
+ before(:all) do
+ @feat = MarkdownFeature.new
+
+ # `markdown` expects a `@project` variable
+ @project = @feat.project
+
+ @md = markdown(@feat.raw_markdown)
+ @doc = Nokogiri::HTML::DocumentFragment.parse(@md)
+ end
+
+ after(:all) do
+ @feat.teardown
+ end
+
+ # Given a header ID, goes to that element's parent (the header), then to its
+ # second sibling (the body).
+ def get_section(id)
+ @doc.at_css("##{id}").parent.next_element
+ end
+
+ # it 'writes to a file' do
+ # File.open(Rails.root.join('tmp/capybara/markdown_spec.html'), 'w') do |file|
+ # file.puts @md
+ # end
+ # end
+
+ describe 'Markdown' do
+ describe 'No Intra Emphasis' do
+ it 'does not parse emphasis inside of words' do
+ body = get_section('no-intra-emphasis')
+ expect(body.to_html).not_to match('foo<em>bar</em>baz')
+ end
+ end
+
+ describe 'Tables' do
+ it 'parses table Markdown' do
+ body = get_section('tables')
+ expect(body).to have_selector('th:contains("Header")')
+ expect(body).to have_selector('th:contains("Row")')
+ expect(body).to have_selector('th:contains("Example")')
+ end
+
+ it 'allows Markdown in tables' do
+ expect(@doc.at_css('td:contains("Baz")').children.to_html).
+ to eq '<strong>Baz</strong>'
+ end
+ end
+
+ describe 'Fenced Code Blocks' do
+ it 'parses fenced code blocks' do
+ expect(@doc).to have_selector('pre.code.highlight.white.c')
+ expect(@doc).to have_selector('pre.code.highlight.white.python')
+ end
+ end
+
+ describe 'Strikethrough' do
+ it 'parses strikethroughs' do
+ expect(@doc).to have_selector(%{del:contains("and this text doesn't")})
+ end
+ end
+
+ describe 'Superscript' do
+ it 'parses superscript' do
+ body = get_section('superscript')
+ expect(body.to_html).to match('1<sup>st</sup>')
+ expect(body.to_html).to match('2<sup>nd</sup>')
+ end
+ end
+ end
+
+ describe 'HTML::Pipeline' do
+ describe 'SanitizationFilter' do
+ it 'uses a permissive whitelist' do
+ expect(@doc).to have_selector('b#manual-b')
+ expect(@doc).to have_selector('em#manual-em')
+ expect(@doc).to have_selector("code#manual-code")
+ expect(@doc).to have_selector('kbd:contains("s")')
+ expect(@doc).to have_selector('strike:contains(Emoji)')
+ expect(@doc).to have_selector('img#manual-img')
+ expect(@doc).to have_selector('br#manual-br')
+ expect(@doc).to have_selector('hr#manual-hr')
+ end
+
+ it 'permits span elements' do
+ expect(@doc).to have_selector('span#span-class-light.light')
+ end
+
+ it 'permits table alignment' do
+ expect(@doc.at_css('th:contains("Header")')['style']).to eq 'text-align: center'
+ expect(@doc.at_css('th:contains("Row")')['style']).to eq 'text-align: right'
+ expect(@doc.at_css('th:contains("Example")')['style']).to eq 'text-align: left'
+
+ expect(@doc.at_css('td:contains("Foo")')['style']).to eq 'text-align: center'
+ expect(@doc.at_css('td:contains("Bar")')['style']).to eq 'text-align: right'
+ expect(@doc.at_css('td:contains("Baz")')['style']).to eq 'text-align: left'
+ end
+
+ it 'removes `rel` attribute from links' do
+ expect(@doc).to have_selector('a#a-rel-nofollow')
+ expect(@doc).not_to have_selector('a#a-rel-nofollow[rel]')
+ end
+
+ it "removes `href` from `a` elements if it's fishy" do
+ expect(@doc).to have_selector('a#a-href-javascript')
+ expect(@doc).not_to have_selector('a#a-href-javascript[href]')
+ end
+ end
+
+ describe 'Escaping' do
+ let(:table) { @doc.css('table').last.at_css('tbody') }
+
+ it 'escapes non-tag angle brackets' do
+ expect(table.at_xpath('.//tr[1]/td[3]').inner_html).to eq '1 &lt; 3 &amp; 5'
+ end
+ end
+
+ describe 'Edge Cases' do
+ it 'allows markup inside link elements' do
+ expect(@doc.at_css('a[href="#link-emphasis"]').to_html).
+ to eq %{<a href="#link-emphasis"><em>text</em></a>}
+
+ expect(@doc.at_css('a[href="#link-strong"]').to_html).
+ to eq %{<a href="#link-strong"><strong>text</strong></a>}
+
+ expect(@doc.at_css('a[href="#link-code"]').to_html).
+ to eq %{<a href="#link-code"><code>text</code></a>}
+ end
+ end
+
+ describe 'EmojiFilter' do
+ it 'parses Emoji' do
+ expect(@doc).to have_selector('img.emoji', count: 10)
+ end
+ end
+
+ describe 'TableOfContentsFilter' do
+ it 'creates anchors inside header elements' do
+ expect(@doc).to have_selector('h1 a#gitlab-markdown')
+ expect(@doc).to have_selector('h2 a#markdown')
+ expect(@doc).to have_selector('h3 a#autolinkfilter')
+ end
+ end
+
+ describe 'AutolinkFilter' do
+ let(:list) { get_section('autolinkfilter').next_element }
+
+ def item(index)
+ list.at_css("li:nth-child(#{index})")
+ end
+
+ it 'autolinks http://' do
+ expect(item(1).children.first.name).to eq 'a'
+ expect(item(1).children.first['href']).to eq 'http://about.gitlab.com/'
+ end
+
+ it 'autolinks https://' do
+ expect(item(2).children.first.name).to eq 'a'
+ expect(item(2).children.first['href']).to eq 'https://google.com/'
+ end
+
+ it 'autolinks ftp://' do
+ expect(item(3).children.first.name).to eq 'a'
+ expect(item(3).children.first['href']).to eq 'ftp://ftp.us.debian.org/debian/'
+ end
+
+ it 'autolinks smb://' do
+ expect(item(4).children.first.name).to eq 'a'
+ expect(item(4).children.first['href']).to eq 'smb://foo/bar/baz'
+ end
+
+ it 'autolinks irc://' do
+ expect(item(5).children.first.name).to eq 'a'
+ expect(item(5).children.first['href']).to eq 'irc://irc.freenode.net/git'
+ end
+
+ it 'autolinks short, invalid URLs' do
+ expect(item(6).children.first.name).to eq 'a'
+ expect(item(6).children.first['href']).to eq 'http://localhost:3000'
+ end
+
+ %w(code a kbd).each do |elem|
+ it "ignores links inside '#{elem}' element" do
+ expect(@doc.at_css("#{elem}#autolink-#{elem}").child).to be_text
+ end
+ end
+ end
+
+ describe 'ReferenceFilter' do
+ it 'handles references in headers' do
+ header = @doc.at_css('#reference-filters-eg-1').parent
+
+ expect(header.css('a').size).to eq 2
+ end
+
+ it "handles references in Markdown" do
+ body = get_section('reference-filters-eg-1')
+ expect(body).to have_selector('em a.gfm-merge_request', count: 1)
+ end
+
+ it 'parses user references' do
+ body = get_section('userreferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-project_member', count: 3)
+ end
+
+ it 'parses issue references' do
+ body = get_section('issuereferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-issue', count: 2)
+ end
+
+ it 'parses merge request references' do
+ body = get_section('mergerequestreferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-merge_request', count: 2)
+ end
+
+ it 'parses snippet references' do
+ body = get_section('snippetreferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-snippet', count: 2)
+ end
+
+ it 'parses commit range references' do
+ body = get_section('commitrangereferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-commit_range', count: 2)
+ end
+
+ it 'parses commit references' do
+ body = get_section('commitreferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-commit', count: 2)
+ end
+
+ it 'parses label references' do
+ body = get_section('labelreferencefilter')
+ expect(body).to have_selector('a.gfm.gfm-label', count: 3)
+ end
+ end
+
+ describe 'Task Lists' do
+ it 'generates task lists' do
+ body = get_section('task-lists')
+ expect(body).to have_selector('ul.task-list', count: 2)
+ expect(body).to have_selector('li.task-list-item', count: 7)
+ expect(body).to have_selector('input[checked]', count: 3)
+ end
+ end
+ end
+end
+
+# This is a helper class used by the GitLab Markdown feature spec
+#
+# Because the feature spec only cares about the output of the Markdown, and the
+# test setup and teardown and parsing is fairly expensive, we only want to do it
+# once. Unfortunately RSpec will not let you access `let`s in a `before(:all)`
+# block, so we fake it by encapsulating all the shared setup in this class.
+#
+# The class renders `spec/fixtures/markdown.md.erb` using ERB, allowing for
+# reference to the factory-created objects.
+class MarkdownFeature
+ include FactoryGirl::Syntax::Methods
+
+ def initialize
+ DatabaseCleaner.start
+ end
+
+ def teardown
+ DatabaseCleaner.clean
+ end
+
+ def user
+ @user ||= create(:user)
+ end
+
+ def group
+ unless @group
+ @group = create(:group)
+ @group.add_user(user, Gitlab::Access::DEVELOPER)
+ end
+
+ @group
+ end
+
+ # Direct references ----------------------------------------------------------
+
+ def project
+ @project ||= create(:project)
+ end
+
+ def issue
+ @issue ||= create(:issue, project: project)
+ end
+
+ def merge_request
+ @merge_request ||= create(:merge_request, :simple, source_project: project)
+ end
+
+ def snippet
+ @snippet ||= create(:project_snippet, project: project)
+ end
+
+ def commit
+ @commit ||= project.repository.commit
+ end
+
+ def commit_range
+ unless @commit_range
+ commit2 = project.repository.commit('HEAD~3')
+ @commit_range = CommitRange.new("#{commit.id}...#{commit2.id}")
+ end
+
+ @commit_range
+ end
+
+ def simple_label
+ @simple_label ||= create(:label, name: 'gfm', project: project)
+ end
+
+ def label
+ @label ||= create(:label, name: 'awaiting feedback', project: project)
+ end
+
+ # Cross-references -----------------------------------------------------------
+
+ def xproject
+ unless @xproject
+ namespace = create(:namespace, name: 'cross-reference')
+ @xproject = create(:project, namespace: namespace)
+ @xproject.team << [user, :developer]
+ end
+
+ @xproject
+ end
+
+ # Shortcut to "cross-reference/project"
+ def xref
+ xproject.path_with_namespace
+ end
+
+ def xissue
+ @xissue ||= create(:issue, project: xproject)
+ end
+
+ def xmerge_request
+ @xmerge_request ||= create(:merge_request, :simple, source_project: xproject)
+ end
+
+ def xsnippet
+ @xsnippet ||= create(:project_snippet, project: xproject)
+ end
+
+ def xcommit
+ @xcommit ||= xproject.repository.commit
+ end
+
+ def xcommit_range
+ unless @xcommit_range
+ xcommit2 = xproject.repository.commit('HEAD~2')
+ @xcommit_range = CommitRange.new("#{xcommit.id}...#{xcommit2.id}")
+ end
+
+ @xcommit_range
+ end
+
+ def raw_markdown
+ fixture = Rails.root.join('spec/fixtures/markdown.md.erb')
+ ERB.new(File.read(fixture)).result(binding)
+ end
+end
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 3d2d8a3502c..67238e3ab76 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -25,8 +25,8 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_denied_for :visitor }
end
- describe "GET /dashboard/projects" do
- subject { projects_dashboard_path }
+ describe "GET /dashboard/projects/starred" do
+ subject { starred_dashboard_projects_path }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for :user }
diff --git a/spec/features/security/group/group_access_spec.rb b/spec/features/security/group/group_access_spec.rb
index e0c5cbf4d3d..63793149459 100644
--- a/spec/features/security/group/group_access_spec.rb
+++ b/spec/features/security/group/group_access_spec.rb
@@ -59,8 +59,8 @@ describe "Group access", feature: true do
it { is_expected.to be_denied_for :visitor }
end
- describe "GET /groups/:path/members" do
- subject { members_group_path(group) }
+ describe "GET /groups/:path/group_members" do
+ subject { group_group_members_path(group) }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
diff --git a/spec/features/security/group/internal_group_access_spec.rb b/spec/features/security/group/internal_group_access_spec.rb
index 5279a1bc13a..d17a7412e43 100644
--- a/spec/features/security/group/internal_group_access_spec.rb
+++ b/spec/features/security/group/internal_group_access_spec.rb
@@ -55,8 +55,8 @@ describe "Group with internal project access", feature: true do
it { is_expected.to be_denied_for :visitor }
end
- describe "GET /groups/:path/members" do
- subject { members_group_path(group) }
+ describe "GET /groups/:path/group_members" do
+ subject { group_group_members_path(group) }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
diff --git a/spec/features/security/group/mixed_group_access_spec.rb b/spec/features/security/group/mixed_group_access_spec.rb
index efd14858b98..b3db7b5dea4 100644
--- a/spec/features/security/group/mixed_group_access_spec.rb
+++ b/spec/features/security/group/mixed_group_access_spec.rb
@@ -56,8 +56,8 @@ describe "Group access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
- describe "GET /groups/:path/members" do
- subject { members_group_path(group) }
+ describe "GET /groups/:path/group_members" do
+ subject { group_group_members_path(group) }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
diff --git a/spec/features/security/group/public_group_access_spec.rb b/spec/features/security/group/public_group_access_spec.rb
index c7e3d0a8a40..c16f0c0d1e1 100644
--- a/spec/features/security/group/public_group_access_spec.rb
+++ b/spec/features/security/group/public_group_access_spec.rb
@@ -55,8 +55,8 @@ describe "Group with public project access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
- describe "GET /groups/:path/members" do
- subject { members_group_path(group) }
+ describe "GET /groups/:path/group_members" do
+ subject { group_group_members_path(group) }
it { is_expected.to be_allowed_for owner }
it { is_expected.to be_allowed_for master }
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index 322697bced8..8d1bfd25223 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -79,8 +79,8 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for :visitor }
end
- describe "GET /:project_path/team" do
- subject { namespace_project_team_index_path(project.namespace, project) }
+ describe "GET /:project_path/project_members" do
+ subject { namespace_project_project_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_denied_for reporter }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index ea146c3f0e4..9021ff33186 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -79,8 +79,8 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for :visitor }
end
- describe "GET /:project_path/team" do
- subject { namespace_project_team_index_path(project.namespace, project) }
+ describe "GET /:project_path/project_members" do
+ subject { namespace_project_project_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_denied_for reporter }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 8ee9199ff29..6ec190ed777 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -84,8 +84,8 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
- describe "GET /:project_path/team" do
- subject { namespace_project_team_index_path(project.namespace, project) }
+ describe "GET /:project_path/project_members" do
+ subject { namespace_project_project_members_path(project.namespace, project) }
it { is_expected.to be_allowed_for master }
it { is_expected.to be_denied_for reporter }
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
new file mode 100644
index 00000000000..2099fc40cca
--- /dev/null
+++ b/spec/features/task_lists_spec.rb
@@ -0,0 +1,151 @@
+require 'spec_helper'
+
+feature 'Task Lists' do
+ include Warden::Test::Helpers
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+
+ let(:markdown) do
+ <<-MARKDOWN.strip_heredoc
+ This is a task list:
+
+ - [ ] Incomplete entry 1
+ - [x] Complete entry 1
+ - [ ] Incomplete entry 2
+ - [x] Complete entry 2
+ - [ ] Incomplete entry 3
+ - [ ] Incomplete entry 4
+ MARKDOWN
+ end
+
+ before do
+ Warden.test_mode!
+
+ project.team << [user, :master]
+ project.team << [user2, :guest]
+
+ login_as(user)
+ end
+
+ def visit_issue(project, issue)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ describe 'for Issues' do
+ let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_issue(project, issue)
+
+ container = '.issue-details .description.js-task-list-container'
+
+ expect(page).to have_selector(container)
+ expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .js-task-list-field")
+ expect(page).to have_selector('form.js-issue-update')
+ expect(page).to have_selector('a.btn-close')
+ end
+
+ it 'is only editable by author' do
+ visit_issue(project, issue)
+ expect(page).to have_selector('.js-task-list-container')
+
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+ end
+ end
+
+ describe 'for Notes' do
+ let!(:issue) { create(:issue, author: user, project: project) }
+ let!(:note) { create(:note, note: markdown, noteable: issue, author: user) }
+
+ it 'renders for note body' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 6)
+ expect(page).to have_selector('.note ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('.note .js-task-list-container')
+ expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
+ expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+ end
+
+ it 'is only editable by author' do
+ visit_issue(project, issue)
+ expect(page).to have_selector('.js-task-list-container')
+
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+ end
+
+ describe 'for Merge Requests' do
+ def visit_merge_request(project, merge)
+ visit namespace_project_merge_request_path(project.namespace, project, merge)
+ end
+
+ let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
+
+ it 'renders for description' do
+ visit_merge_request(project, merge)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_merge_request(project, merge)
+
+ container = '.merge-request-details .description.js-task-list-container'
+
+ expect(page).to have_selector(container)
+ expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .js-task-list-field")
+ expect(page).to have_selector('form.js-merge-request-update')
+ expect(page).to have_selector('a.btn-close')
+ end
+
+ it 'is only editable by author' do
+ visit_merge_request(project, merge)
+ expect(page).to have_selector('.js-task-list-container')
+
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+ end
+ end
+end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index 21a3a4bf937..93d2b18b5fc 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,14 +1,30 @@
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
+ 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/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 479fa950387..69bac387d20 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -43,7 +43,7 @@ describe IssuesFinder do
end
it 'should filter by milestone id' do
- params = { scope: "all", milestone_id: milestone.id, state: 'opened' }
+ params = { scope: "all", milestone_title: milestone.title, state: 'opened' }
issues = IssuesFinder.new.execute(user, params)
expect(issues).to eq([issue1])
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/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb
new file mode 100644
index 00000000000..bc023ecf793
--- /dev/null
+++ b/spec/fixtures/markdown.md.erb
@@ -0,0 +1,196 @@
+# GitLab Markdown
+
+This document is intended to be a comprehensive example of custom GitLab
+Markdown usage. It will be parsed and then tested for accuracy. Let's get
+started.
+
+## Markdown
+
+GitLab uses [Redcarpet](http://git.io/ld_NVQ) to parse all Markdown into
+HTML.
+
+It has some special features. Let's try 'em out!
+
+### No Intra Emphasis
+
+This string should have no emphasis: foo_bar_baz
+
+### Tables
+
+| Header | Row | Example |
+| :------: | ---: | :------ |
+| Foo | Bar | **Baz** |
+
+### Fenced Code Blocks
+
+```c
+#include<stdio.h>
+
+main()
+{
+ printf("Hello World");
+
+}
+```
+
+```python
+print "Hello, World!"
+```
+
+### Strikethrough
+
+This text says this, ~~and this text doesn't~~.
+
+### Superscript
+
+This is my 1^(st) time using superscript in Markdown. Now this is my
+2^(nd).
+
+### Next step
+
+After the Markdown has been turned into HTML, it gets passed through...
+
+## HTML::Pipeline
+
+### SanitizationFilter
+
+GitLab uses <a href="http://git.io/vfW8a" class="sanitize" id="sanitize-link">HTML::Pipeline::SanitizationFilter</a>
+to sanitize the generated HTML, stripping dangerous or unwanted tags.
+
+Its default whitelist is pretty permissive. Check it:
+
+<b id="manual-b">This text is bold</b> and <em id="manual-em">this text is emphasized</em>.
+
+<code id="manual-code">echo "Hello, world!"</code>
+
+Press <kbd>s</kbd> to search.
+
+<strike>Emoji</strike> Plain old images! <img
+src="http://www.emoji-cheat-sheet.com/graphics/emojis/smile.png" width="20"
+height="20" id="manual-img" />
+
+Here comes a line break:
+
+<br id="manual-br" />
+
+And a horizontal rule:
+
+<hr id="manual-hr" />
+
+As permissive as it is, we've allowed even more stuff:
+
+<span class="light" id="span-class-light">Span elements</span>
+
+<a href="#" rel="nofollow" id="a-rel-nofollow">This is a link with a defined rel attribute, which should be removed</a>
+
+<a href="javascript:alert('Hi')" id="a-href-javascript">This is a link trying to be sneaky. It gets its link removed entirely.</a>
+
+### Escaping
+
+The problem with SanitizationFilter is that it can be too aggressive.
+
+| Input | Expected | Actual |
+| ----------- | ---------------- | --------- |
+| `1 < 3 & 5` | 1 &lt; 3 &amp; 5 | 1 < 3 & 5 |
+| `<foo>` | &lt;foo&gt; | <foo> |
+
+### Edge Cases
+
+Markdown should be usable inside a link. Let's try!
+
+- [_text_](#link-emphasis)
+- [**text**](#link-strong)
+- [`text`](#link-code)
+
+### EmojiFilter
+
+Because life would be :zzz: without Emoji, right? :rocket:
+
+Get ready for the Emoji :bomb:: :+1::-1::ok_hand::wave::v::raised_hand::muscle:
+
+### TableOfContentsFilter
+
+All headers in this document should be linkable. Try it.
+
+### AutolinkFilter
+
+These are all plain text that should get turned into links:
+
+- http://about.gitlab.com/
+- https://google.com/
+- ftp://ftp.us.debian.org/debian/
+- smb://foo/bar/baz
+- irc://irc.freenode.net/git
+- http://localhost:3000
+
+But it shouldn't autolink text inside certain tags:
+
+- <code id="autolink-code">http://about.gitlab.com/</code>
+- <a id="autolink-a">http://about.gitlab.com/</a>
+- <kbd id="autolink-kbd">http://about.gitlab.com/</kbd>
+
+### Reference Filters (e.g., #<%= issue.iid %>)
+
+References should be parseable even inside _!<%= merge_request.iid %>_ emphasis.
+
+#### UserReferenceFilter
+
+- All: @all
+- User: @<%= user.username %>
+- Group: @<%= group.name %>
+- Ignores invalid: @fake_user
+- Ignored in code: `@<%= user.username %>`
+- Ignored in links: [Link to @<%= user.username %>](#user-link)
+
+#### IssueReferenceFilter
+
+- Issue: #<%= issue.iid %>
+- Issue in another project: <%= xref %>#<%= xissue.iid %>
+- Ignored in code: `#<%= issue.iid %>`
+- Ignored in links: [Link to #<%= issue.iid %>](#issue-link)
+
+#### MergeRequestReferenceFilter
+
+- Merge request: !<%= merge_request.iid %>
+- Merge request in another project: <%= xref %>!<%= xmerge_request.iid %>
+- Ignored in code: `!<%= merge_request.iid %>`
+- Ignored in links: [Link to !<%= merge_request.iid %>](#merge-request-link)
+
+#### SnippetReferenceFilter
+
+- Snippet: $<%= snippet.id %>
+- Snippet in another project: <%= xref %>$<%= xsnippet.id %>
+- Ignored in code: `$<%= snippet.id %>`
+- Ignored in links: [Link to $<%= snippet.id %>](#snippet-link)
+
+#### CommitRangeReferenceFilter
+
+- Range: <%= commit_range %>
+- Range in another project: <%= xref %>@<%= xcommit_range %>
+- Ignored in code: `<%= commit_range %>`
+- Ignored in links: [Link to <%= commit_range %>](#commit-range-link)
+
+#### CommitReferenceFilter
+
+- Commit: <%= commit.id %>
+- Commit in another project: <%= xref %>@<%= xcommit.id %>
+- Ignored in code: `<%= commit.id %>`
+- Ignored in links: [Link to <%= commit.id %>](#commit-link)
+
+#### LabelReferenceFilter
+
+- Label by ID: ~<%= simple_label.id %>
+- Label by name: ~<%= simple_label.name %>
+- Label by name in quotes: ~"<%= label.name %>"
+- Ignored in code: `~<%= simple_label.name %>`
+- Ignored in links: [Link to ~<%= simple_label.id %>](#label-link)
+
+### Task Lists
+
+- [ ] Incomplete task 1
+- [x] Complete task 1
+- [ ] Incomplete task 2
+ - [ ] Incomplete sub-task 1
+ - [ ] Incomplete sub-task 2
+ - [x] Complete sub-task 1
+- [X] Complete task 2
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 99ff8a32ea5..d4cf6540080 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -39,24 +39,6 @@ describe ApplicationHelper do
end
end
- describe 'group_icon' do
- avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png')
-
- it 'should return an url for the avatar' do
- group = create(:group)
- group.avatar = File.open(avatar_file_path)
- group.save!
- expect(group_icon(group.path).to_s).
- to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png")
- end
-
- it 'should give default avatar_icon when no avatar is present' do
- group = create(:group)
- group.save!
- expect(group_icon(group.path)).to match('group_avatar.png')
- end
- end
-
describe 'project_icon' do
avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png')
@@ -243,25 +225,39 @@ 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 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 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
- 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 not raise an error when given a bad URI' do
+ expect { link_to('default', 'if real=1 RANDOM; if real>1 IDLHS; if real>500 LHS') }.
+ not_to raise_error
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 raise an error when given a bad mailto URL' do
+ expect { link_to('email', 'mailto://foo.bar@example.es?subject=Subject%20Line') }.
+ not_to raise_error
end
end
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index 5bd09793b11..dd4c1d645e2 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -4,8 +4,9 @@ describe DiffHelper do
include RepoHelpers
let(:project) { create(:project) }
- let(:commit) { project.repository.commit(sample_commit.id) }
- let(:diff) { commit.diffs.first }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:diffs) { commit.diffs }
+ let(:diff) { diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff) }
describe 'diff_hard_limit_enabled?' do
@@ -30,6 +31,57 @@ describe DiffHelper do
end
end
+ describe 'allowed_diff_lines' do
+ it 'should return hard limit for number of lines in a diff if force diff is true' do
+ allow(controller).to receive(:params) { { force_show_diff: true } }
+ expect(allowed_diff_lines).to eq(50000)
+ end
+
+ it 'should return safe limit for numbers of lines a diff if force diff is false' do
+ expect(allowed_diff_lines).to eq(5000)
+ end
+ end
+
+ describe 'safe_diff_files' do
+ it 'should return all files from a commit that is smaller than safe limits' do
+ expect(safe_diff_files(diffs).length).to eq(2)
+ end
+
+ it 'should return only the first file if the diff line count in the 2nd file takes the total beyond safe limits' do
+ diffs[1].diff.stub(lines: [""] * 4999) #simulate 4999 lines
+ expect(safe_diff_files(diffs).length).to eq(1)
+ end
+
+ it 'should return all files from a commit that is beyond safe limit for numbers of lines if force diff is true' do
+ allow(controller).to receive(:params) { { force_show_diff: true } }
+ diffs[1].diff.stub(lines: [""] * 4999) #simulate 4999 lines
+ expect(safe_diff_files(diffs).length).to eq(2)
+ end
+
+ it 'should return only the first file if the diff line count in the 2nd file takes the total beyond hard limits' do
+ allow(controller).to receive(:params) { { force_show_diff: true } }
+ diffs[1].diff.stub(lines: [""] * 49999) #simulate 49999 lines
+ expect(safe_diff_files(diffs).length).to eq(1)
+ end
+
+ it 'should return only a safe number of file diffs if a commit touches more files than the safe limits' do
+ large_diffs = diffs * 100 #simulate 200 diffs
+ expect(safe_diff_files(large_diffs).length).to eq(100)
+ end
+
+ it 'should return all file diffs if a commit touches more files than the safe limits but force diff is true' do
+ allow(controller).to receive(:params) { { force_show_diff: true } }
+ large_diffs = diffs * 100 #simulate 200 diffs
+ expect(safe_diff_files(large_diffs).length).to eq(200)
+ end
+
+ it 'should return a limited file diffs if a commit touches more files than the hard limits and force diff is true' do
+ allow(controller).to receive(:params) { { force_show_diff: true } }
+ very_large_diffs = diffs * 1000 #simulate 2000 diffs
+ expect(safe_diff_files(very_large_diffs).length).to eq(1000)
+ end
+ end
+
describe 'parallel_diff' do
it 'should return an array of arrays containing the parsed diff' do
expect(parallel_diff(diff_file, 0)).
diff --git a/spec/helpers/events_helper_spec.rb b/spec/helpers/events_helper_spec.rb
index c4a192ac1aa..b392371deb4 100644
--- a/spec/helpers/events_helper_spec.rb
+++ b/spec/helpers/events_helper_spec.rb
@@ -4,6 +4,8 @@ describe EventsHelper do
include ApplicationHelper
include GitlabMarkdownHelper
+ let(:current_user) { create(:user, email: "current@email.com") }
+
it 'should display one line of plain text without alteration' do
input = 'A short, plain note'
expect(event_note(input)).to match(input)
@@ -50,4 +52,14 @@ describe EventsHelper do
expect(event_note(input)).to match(link_url)
expect(event_note(input)).to match(expected_link_text)
end
+
+ it 'should preserve code color scheme' do
+ input = "```ruby\ndef test\n 'hello world'\nend\n```"
+ expected = '<pre class="code highlight white ruby">' \
+ "<code><span class=\"k\">def</span> <span class=\"nf\">test</span>\n" \
+ " <span class=\"s1\">\'hello world\'</span>\n" \
+ "<span class=\"k\">end</span>\n" \
+ '</code></pre>'
+ expect(event_note(input)).to eq(expected)
+ end
end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 74a42932fe8..2f67879efdc 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -2,441 +2,27 @@ require 'spec_helper'
describe GitlabMarkdownHelper do
include ApplicationHelper
- include IssuesHelper
let!(:project) { create(:project) }
- let(:empty_project) { create(:empty_project) }
let(:user) { create(:user, username: 'gfm') }
- let(:commit) { project.repository.commit }
- let(:earlier_commit){ project.repository.commit("HEAD~2") }
+ let(:commit) { project.commit }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) }
- let(:member) { project.project_members.where(user_id: user).first }
- def url_helper(image_name)
- File.join(root_url, 'assets', image_name)
- end
+ # Helper expects a current_user method.
+ let(:current_user) { user }
before do
# Helper expects a @project instance variable
@project = project
- @ref = 'markdown'
- @repository = project.repository
- @request.host = Gitlab.config.gitlab.host
end
describe "#gfm" do
- it "should return unaltered text if project is nil" do
- actual = "Testing references: ##{issue.iid}"
-
- expect(gfm(actual)).not_to eq(actual)
-
- @project = nil
- expect(gfm(actual)).to eq(actual)
- end
-
- it "should not alter non-references" do
- actual = expected = "_Please_ *stop* 'helping' and all the other b*$#%' you do."
- expect(gfm(actual)).to eq(expected)
- end
-
- it "should not touch HTML entities" do
- allow(@project.issues).to receive(:where).
- with(id: '39').and_return([issue])
- actual = 'We&#39;ll accept good pull requests.'
- expect(gfm(actual)).to eq("We'll accept good pull requests.")
- end
-
it "should forward HTML options to links" do
expect(gfm("Fixed in #{commit.id}", @project, class: 'foo')).
- to have_selector('a.gfm.foo')
- end
-
- describe "referencing a commit range" do
- let(:expected) { namespace_project_compare_path(project.namespace, project, from: earlier_commit.id, to: commit.id) }
-
- it "should link using a full id" do
- actual = "What happened in #{earlier_commit.id}...#{commit.id}"
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link using a short id" do
- actual = "What happened in #{earlier_commit.short_id}...#{commit.short_id}"
- expected = namespace_project_compare_path(project.namespace, project, from: earlier_commit.short_id, to: commit.short_id)
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link inclusively" do
- actual = "What happened in #{earlier_commit.id}..#{commit.id}"
- expected = namespace_project_compare_path(project.namespace, project, from: "#{earlier_commit.id}^", to: commit.id)
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- actual = "(see #{earlier_commit.id}...#{commit.id})"
- expect(gfm(actual)).to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Changes #{earlier_commit.id}...#{commit.id} dramatically"
- expected = /Changes <a.+>#{earlier_commit.id}...#{commit.id}<\/a> dramatically/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- actual = expected = "What happened in #{earlier_commit.id.reverse}...#{commit.id.reverse}"
- expect(gfm(actual)).to eq(expected)
- end
-
- it "should include a title attribute" do
- actual = "What happened in #{earlier_commit.id}...#{commit.id}"
- expect(gfm(actual)).to match(/title="Commits #{earlier_commit.id} through #{commit.id}"/)
- end
-
- it "should include standard gfm classes" do
- actual = "What happened in #{earlier_commit.id}...#{commit.id}"
- expect(gfm(actual)).to match(/class="\s?gfm gfm-commit_range\s?"/)
- end
- end
-
- describe "referencing a commit" do
- let(:expected) { namespace_project_commit_path(project.namespace, project, commit) }
-
- it "should link using a full id" do
- actual = "Reverts #{commit.id}"
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link using a short id" do
- actual = "Backported from #{commit.short_id}"
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- actual = "Reverted (see #{commit.id})"
- expect(gfm(actual)).to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Changes #{commit.id} dramatically"
- expected = /Changes <a.+>#{commit.id}<\/a> dramatically/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- actual = expected = "What happened in #{commit.id.reverse}"
- expect(gfm(actual)).to eq(expected)
- end
-
- it "should include a title attribute" do
- actual = "Reverts #{commit.id}"
- expect(gfm(actual)).to match(/title="#{commit.link_title}"/)
- end
-
- it "should include standard gfm classes" do
- actual = "Reverts #{commit.id}"
- expect(gfm(actual)).to match(/class="\s?gfm gfm-commit\s?"/)
- end
- end
-
- describe "referencing a team member" do
- let(:actual) { "@#{user.username} you are right." }
- let(:expected) { user_path(user) }
-
- before do
- project.team << [user, :master]
- end
-
- it "should link using a simple name" do
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link using a name with dots" do
- user.update_attributes(name: "alphA.Beta")
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link using name with underscores" do
- user.update_attributes(name: "ping_pong_king")
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- actual = "Mail the admin (@#{user.username})"
- expect(gfm(actual)).to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Yes, @#{user.username} is right."
- expected = /Yes, <a.+>@#{user.username}<\/a> is right/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- actual = expected = "@#{user.username.reverse} you are right."
- expect(gfm(actual)).to eq(expected)
- end
-
- it "should include standard gfm classes" do
- expect(gfm(actual)).to match(/class="\s?gfm gfm-team_member\s?"/)
- end
- end
-
- # Shared examples for referencing an object
- #
- # Expects the following attributes to be available in the example group:
- #
- # - object - The object itself
- # - reference - The object reference string (e.g., #1234, $1234, !1234)
- #
- # Currently limited to Snippets, Issues and MergeRequests
- shared_examples 'referenced object' do
- let(:actual) { "Reference to #{reference}" }
- let(:expected) { polymorphic_path([project.namespace, project, object]) }
-
- it "should link using a valid id" do
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- # Wrap the reference in parenthesis
- expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
-
- # Append some text to the end of the reference
- expect(gfm(actual.gsub(reference, "#{reference}, right?"))).
- to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Referenced #{reference} already."
- expected = /Referenced <a.+>[^\s]+<\/a> already/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- # Modify the reference string so it's still parsed, but is invalid
- reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
- expect(gfm(actual)).to eq(actual)
- end
-
- it "should include a title attribute" do
- title = "#{object.class.to_s.titlecase}: #{object.title}"
- expect(gfm(actual)).to match(/title="#{title}"/)
- end
-
- it "should include standard gfm classes" do
- css = object.class.to_s.underscore
- expect(gfm(actual)).to match(/class="\s?gfm gfm-#{css}\s?"/)
- end
- end
-
- # Shared examples for referencing an object in a different project
- #
- # Expects the following attributes to be available in the example group:
- #
- # - object - The object itself
- # - reference - The object reference string (e.g., #1234, $1234, !1234)
- # - other_project - The project that owns the target object
- #
- # Currently limited to Snippets, Issues and MergeRequests
- shared_examples 'cross-project referenced object' do
- let(:project_path) { @other_project.path_with_namespace }
- let(:full_reference) { "#{project_path}#{reference}" }
- let(:actual) { "Reference to #{full_reference}" }
- let(:expected) do
- if object.is_a?(Commit)
- namespace_project_commit_path(@other_project.namespace, @other_project, object)
- else
- polymorphic_path([@other_project.namespace, @other_project, object])
- end
- end
-
- it 'should link using a valid id' do
- expect(gfm(actual)).to match(
- /#{expected}.*#{Regexp.escape(full_reference)}/
- )
- end
-
- it 'should link with adjacent text' do
- # Wrap the reference in parenthesis
- expect(gfm(actual.gsub(full_reference, "(#{full_reference})"))).to(
- match(expected)
- )
-
- # Append some text to the end of the reference
- expect(gfm(actual.gsub(full_reference, "#{full_reference}, right?"))).
- to(match(expected))
- end
-
- it 'should keep whitespace intact' do
- actual = "Referenced #{full_reference} already."
- expected = /Referenced <a.+>[^\s]+<\/a> already/
- expect(gfm(actual)).to match(expected)
- end
-
- it 'should not link with an invalid id' do
- # Modify the reference string so it's still parsed, but is invalid
- if object.is_a?(Commit)
- reference.gsub!(/^(.).+$/, '\1' + '12345abcd')
- else
- reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
- end
- expect(gfm(actual)).to eq(actual)
- end
-
- it 'should include a title attribute' do
- if object.is_a?(Commit)
- title = object.link_title
- else
- title = "#{object.class.to_s.titlecase}: #{object.title}"
- end
- expect(gfm(actual)).to match(/title="#{title}"/)
- end
-
- it 'should include standard gfm classes' do
- css = object.class.to_s.underscore
- expect(gfm(actual)).to match(/class="\s?gfm gfm-#{css}\s?"/)
- end
- end
-
- describe "referencing an issue" do
- let(:object) { issue }
- let(:reference) { "##{issue.iid}" }
-
- include_examples 'referenced object'
- end
-
- context 'cross-repo references' do
- before(:all) do
- @other_project = create(:project, :public)
- @commit2 = @other_project.repository.commit
- @issue2 = create(:issue, project: @other_project)
- @merge_request2 = create(:merge_request,
- source_project: @other_project,
- target_project: @other_project)
- end
-
- describe 'referencing an issue in another project' do
- let(:object) { @issue2 }
- let(:reference) { "##{@issue2.iid}" }
-
- include_examples 'cross-project referenced object'
- end
-
- describe 'referencing an merge request in another project' do
- let(:object) { @merge_request2 }
- let(:reference) { "!#{@merge_request2.iid}" }
-
- include_examples 'cross-project referenced object'
- end
-
- describe 'referencing a commit in another project' do
- let(:object) { @commit2 }
- let(:reference) { "@#{@commit2.id}" }
-
- include_examples 'cross-project referenced object'
- end
- end
-
- describe "referencing a Jira issue" do
- let(:actual) { "Reference to JIRA-#{issue.iid}" }
- let(:expected) { "http://jira.example/browse/JIRA-#{issue.iid}" }
- let(:reference) { "JIRA-#{issue.iid}" }
-
- before do
- jira = @project.create_jira_service if @project.jira_service.nil?
- properties = {"title"=>"JIRA tracker", "project_url"=>"http://jira.example/issues/?jql=project=A", "issues_url"=>"http://jira.example/browse/:id", "new_issue_url"=>"http://jira.example/secure/CreateIssue.jspa"}
- jira.update_attributes(properties: properties, active: true)
- end
-
- after do
- @project.jira_service.destroy! unless @project.jira_service.nil?
- end
-
- it "should link using a valid id" do
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- # Wrap the reference in parenthesis
- expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
-
- # Append some text to the end of the reference
- expect(gfm(actual.gsub(reference, "#{reference}, right?"))).
- to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Referenced #{reference} already."
- expected = /Referenced <a.+>[^\s]+<\/a> already/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- # Modify the reference string so it's still parsed, but is invalid
- invalid_reference = actual.gsub(/(\d+)$/, "r45")
- expect(gfm(invalid_reference)).to eq(invalid_reference)
- end
-
- it "should include a title attribute" do
- title = "Issue in JIRA tracker"
- expect(gfm(actual)).to match(/title="#{title}"/)
- end
-
- it "should include standard gfm classes" do
- expect(gfm(actual)).to match(/class="\s?gfm gfm-issue\s?"/)
- end
- end
-
- describe "referencing a merge request" do
- let(:object) { merge_request }
- let(:reference) { "!#{merge_request.iid}" }
-
- include_examples 'referenced object'
- end
-
- describe "referencing a snippet" do
- let(:object) { snippet }
- let(:reference) { "$#{snippet.id}" }
- let(:actual) { "Reference to #{reference}" }
- let(:expected) { namespace_project_snippet_path(project.namespace, project, object) }
-
- it "should link using a valid id" do
- expect(gfm(actual)).to match(expected)
- end
-
- it "should link with adjacent text" do
- # Wrap the reference in parenthesis
- expect(gfm(actual.gsub(reference, "(#{reference})"))).to match(expected)
-
- # Append some text to the end of the reference
- expect(gfm(actual.gsub(reference, "#{reference}, right?"))).to match(expected)
- end
-
- it "should keep whitespace intact" do
- actual = "Referenced #{reference} already."
- expected = /Referenced <a.+>[^\s]+<\/a> already/
- expect(gfm(actual)).to match(expected)
- end
-
- it "should not link with an invalid id" do
- # Modify the reference string so it's still parsed, but is invalid
- reference.gsub!(/^(.)(\d+)$/, '\1' + ('\2' * 2))
- expect(gfm(actual)).to eq(actual)
- end
-
- it "should include a title attribute" do
- title = "Snippet: #{object.title}"
- expect(gfm(actual)).to match(/title="#{title}"/)
- end
-
- it "should include standard gfm classes" do
- css = object.class.to_s.underscore
- expect(gfm(actual)).to match(/class="\s?gfm gfm-snippet\s?"/)
- end
-
+ to have_selector('a.gfm.foo')
end
describe "referencing multiple objects" do
@@ -457,91 +43,50 @@ describe GitlabMarkdownHelper do
expect(gfm(actual)).to match(expected)
end
end
-
- describe "emoji" do
- it "matches at the start of a string" do
- expect(gfm(":+1:")).to match(/<img/)
- end
-
- it "matches at the end of a string" do
- expect(gfm("This gets a :-1:")).to match(/<img/)
- end
-
- it "matches with adjacent text" do
- expect(gfm("+1 (:+1:)")).to match(/<img/)
- end
-
- it "has a title attribute" do
- expect(gfm(":-1:")).to match(/title=":-1:"/)
- end
-
- it "has an alt attribute" do
- expect(gfm(":-1:")).to match(/alt=":-1:"/)
- end
-
- it "has an emoji class" do
- expect(gfm(":+1:")).to match('class="emoji"')
- end
-
- it "sets height and width" do
- actual = gfm(":+1:")
- expect(actual).to match(/width="20"/)
- expect(actual).to match(/height="20"/)
- end
-
- it "keeps whitespace intact" do
- expect(gfm('This deserves a :+1: big time.')).
- to match(/deserves a <img.+> big time/)
- end
-
- it "ignores invalid emoji" do
- expect(gfm(":invalid-emoji:")).not_to match(/<img/)
- end
-
- it "should work independent of reference links (i.e. without @project being set)" do
- @project = nil
- expect(gfm(":+1:")).to match(/<img/)
- end
- end
end
- describe "#link_to_gfm" do
+ describe '#link_to_gfm' do
let(:commit_path) { namespace_project_commit_path(project.namespace, project, commit) }
let(:issues) { create_list(:issue, 2, project: project) }
- it "should handle references nested in links with all the text" do
+ it 'should handle references nested in links with all the text' do
actual = link_to_gfm("This should finally fix ##{issues[0].iid} and ##{issues[1].iid} for real", commit_path)
+ doc = Nokogiri::HTML.parse(actual)
- # Break the result into groups of links with their content, without
- # closing tags
- groups = actual.split("</a>")
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
# Leading commit link
- expect(groups[0]).to match(/href="#{commit_path}"/)
- expect(groups[0]).to match(/This should finally fix $/)
+ expect(doc.css('a')[0].attr('href')).to eq commit_path
+ expect(doc.css('a')[0].text).to eq 'This should finally fix '
# First issue link
- expect(groups[1]).
- to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[0])}"/)
- expect(groups[1]).to match(/##{issues[0].iid}$/)
+ expect(doc.css('a')[1].attr('href')).
+ to eq namespace_project_issue_path(project.namespace, project, issues[0])
+ expect(doc.css('a')[1].text).to eq "##{issues[0].iid}"
# Internal commit link
- expect(groups[2]).to match(/href="#{commit_path}"/)
- expect(groups[2]).to match(/ and /)
+ expect(doc.css('a')[2].attr('href')).to eq commit_path
+ expect(doc.css('a')[2].text).to eq ' and '
# Second issue link
- expect(groups[3]).
- to match(/href="#{namespace_project_issue_path(project.namespace, project, issues[1])}"/)
- expect(groups[3]).to match(/##{issues[1].iid}$/)
+ expect(doc.css('a')[3].attr('href')).
+ to eq namespace_project_issue_path(project.namespace, project, issues[1])
+ expect(doc.css('a')[3].text).to eq "##{issues[1].iid}"
# Trailing commit link
- expect(groups[4]).to match(/href="#{commit_path}"/)
- expect(groups[4]).to match(/ for real$/)
+ expect(doc.css('a')[4].attr('href')).to eq commit_path
+ expect(doc.css('a')[4].text).to eq ' for real'
end
- it "should forward HTML options" do
+ it 'should forward HTML options' do
actual = link_to_gfm("Fixed in #{commit.id}", commit_path, class: 'foo')
- expect(actual).to have_selector 'a.gfm.gfm-commit.foo'
+ doc = Nokogiri::HTML.parse(actual)
+
+ expect(doc.css('a')).to satisfy do |v|
+ # 'foo' gets added to all links
+ v.all? { |a| a.attr('class').match(/foo$/) }
+ end
end
it "escapes HTML passed in as the body" do
@@ -552,197 +97,79 @@ describe GitlabMarkdownHelper do
end
describe "#markdown" do
- it "should handle references in paragraphs" do
- actual = "\n\nLorem ipsum dolor sit amet. #{commit.id} Nam pulvinar sapien eget.\n"
- expected = namespace_project_commit_path(project.namespace, project, commit)
- expect(markdown(actual)).to match(expected)
- end
-
- it "should handle references in headers" do
- actual = "\n# Working around ##{issue.iid}\n## Apply !#{merge_request.iid}"
-
- expect(markdown(actual, no_header_anchors: true)).
- to match(%r{<h1[^<]*>Working around <a.+>##{issue.iid}</a></h1>})
- expect(markdown(actual, no_header_anchors: true)).
- to match(%r{<h2[^<]*>Apply <a.+>!#{merge_request.iid}</a></h2>})
- end
-
- it "should add ids and links to headers" do
- # Test every rule except nested tags.
- text = '..Ab_c-d. e..'
- id = 'ab_c-d-e'
- expect(markdown("# #{text}")).
- to match(%r{<h1 id="#{id}">#{text}<a href="[^"]*##{id}"></a></h1>})
- expect(markdown("# #{text}", {no_header_anchors:true})).
- to eq("<h1>#{text}</h1>")
-
- id = 'link-text'
- expect(markdown("# [link text](url) ![img alt](url)")).to match(
- %r{<h1 id="#{id}"><a href="[^"]*url">link text</a> <img[^>]*><a href="[^"]*##{id}"></a></h1>}
- )
- end
-
- it "should handle references in lists" do
- project.team << [user, :master]
-
- actual = "\n* dark: ##{issue.iid}\n* light by @#{member.user.username}"
-
- expect(markdown(actual)).
- to match(%r{<li>dark: <a.+>##{issue.iid}</a></li>})
- expect(markdown(actual)).
- to match(%r{<li>light by <a.+>@#{member.user.username}</a></li>})
- end
-
- it "should not link the apostrophe to issue 39" do
- project.team << [user, :master]
- allow(project.issues).
- to receive(:where).with(iid: '39').and_return([issue])
-
- actual = "Yes, it is @#{member.user.username}'s task."
- expected = /Yes, it is <a.+>@#{member.user.username}<\/a>'s task/
- expect(markdown(actual)).to match(expected)
- end
-
- it "should not link the apostrophe to issue 39 in code blocks" do
- project.team << [user, :master]
- allow(project.issues).
- to receive(:where).with(iid: '39').and_return([issue])
-
- actual = "Yes, `it is @#{member.user.username}'s task.`"
- expected = /Yes, <code>it is @gfm\'s task.<\/code>/
- expect(markdown(actual)).to match(expected)
- end
-
- it "should handle references in <em>" do
- actual = "Apply _!#{merge_request.iid}_ ASAP"
-
- expect(markdown(actual)).
- to match(%r{Apply <em><a.+>!#{merge_request.iid}</a></em>})
- end
-
- it "should handle tables" do
- actual = %Q{| header 1 | header 2 |
-| -------- | -------- |
-| cell 1 | cell 2 |
-| cell 3 | cell 4 |}
-
- expect(markdown(actual)).to match(/\A<table/)
- end
-
- it "should leave code blocks untouched" do
- allow(helper).to receive(:user_color_scheme_class).and_return(:white)
-
- target_html = "<pre class=\"code highlight white plaintext\"><code>some code from $#{snippet.id}\nhere too\n</code></pre>\n"
-
- expect(helper.markdown("\n some code from $#{snippet.id}\n here too\n")).
- to eq(target_html)
- expect(helper.markdown("\n```\nsome code from $#{snippet.id}\nhere too\n```\n")).
- to eq(target_html)
- end
-
- it "should leave inline code untouched" do
- expect(markdown("\nDon't use `$#{snippet.id}` here.\n")).to eq(
- "<p>Don't use <code>$#{snippet.id}</code> here.</p>\n"
- )
- end
-
- it "should leave ref-like autolinks untouched" do
- expect(markdown("look at http://example.tld/#!#{merge_request.iid}")).to eq("<p>look at <a href=\"http://example.tld/#!#{merge_request.iid}\">http://example.tld/#!#{merge_request.iid}</a></p>\n")
- 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")
- end
-
- it "should leave ref-like src of images untouched" do
- expect(markdown("screen shot: ![some image](http://example.tld/#!#{merge_request.iid})")).to eq("<p>screen shot: <img src=\"http://example.tld/#!#{merge_request.iid}\" alt=\"some image\"></p>\n")
- end
-
- it "should generate absolute urls for refs" do
- expect(markdown("##{issue.iid}")).to include(namespace_project_issue_path(project.namespace, project, issue))
- end
-
- it "should generate absolute urls for emoji" do
- expect(markdown(':smile:')).to(
- include(%(src="#{Gitlab.config.gitlab.url}/assets/emoji/smile.png))
- )
- end
-
- it "should generate absolute urls for emoji if relative url is present" do
- allow(Gitlab.config.gitlab).to receive(:url).and_return('http://localhost/gitlab/root')
- expect(markdown(":smile:")).to include("src=\"http://localhost/gitlab/root/assets/emoji/smile.png")
- end
-
- it "should generate absolute urls for emoji if asset_host is present" do
- allow(Gitlab::Application.config).to receive(:asset_host).and_return("https://cdn.example.com")
- ActionView::Base.any_instance.stub_chain(:config, :asset_host).and_return("https://cdn.example.com")
- expect(markdown(":smile:")).to include("src=\"https://cdn.example.com/assets/emoji/smile.png")
- end
-
+ # TODO (rspeicher): These belong in a relative link filter spec
+ context 'relative links' do
+ context 'with a valid repository' do
+ before do
+ @repository = project.repository
+ @ref = 'markdown'
+ end
- it "should handle relative urls for a file in master" do
- actual = "[GitLab API doc](doc/api/README.md)\n"
- expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should handle relative urls for a file in master" do
+ actual = "[GitLab API doc](doc/api/README.md)\n"
+ expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should handle relative urls for a file in master with an anchor" do
- actual = "[GitLab API doc](doc/api/README.md#section)\n"
- expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md#section\">GitLab API doc</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should handle relative urls for a file in master with an anchor" do
+ actual = "[GitLab API doc](doc/api/README.md#section)\n"
+ expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md#section\">GitLab API doc</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should not handle relative urls for the current file with an anchor" do
- actual = "[GitLab API doc](#section)\n"
- expected = "<p><a href=\"#section\">GitLab API doc</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should not handle relative urls for the current file with an anchor" do
+ actual = "[GitLab API doc](#section)\n"
+ expected = "<p><a href=\"#section\">GitLab API doc</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should handle relative urls for a directory in master" do
- actual = "[GitLab API doc](doc/api)\n"
- expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should handle relative urls for a directory in master" do
+ actual = "[GitLab API doc](doc/api)\n"
+ expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should handle absolute urls" do
- actual = "[GitLab](https://www.gitlab.com)\n"
- expected = "<p><a href=\"https://www.gitlab.com\">GitLab</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should handle absolute urls" do
+ actual = "[GitLab](https://www.gitlab.com)\n"
+ expected = "<p><a href=\"https://www.gitlab.com\">GitLab</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should handle relative urls in reference links for a file in master" do
- actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n"
- expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should handle relative urls in reference links for a file in master" do
+ actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n"
+ expected = "<p><a href=\"/#{project.path_with_namespace}/blob/#{@ref}/doc/api/README.md\">GitLab API doc</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should handle relative urls in reference links for a directory in master" do
- actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n"
- expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc directory</a></p>\n"
- expect(markdown(actual)).to match(expected)
- end
+ it "should handle relative urls in reference links for a directory in master" do
+ actual = "[GitLab API doc directory][GitLab readmes]\n [GitLab readmes]: doc/api/\n"
+ expected = "<p><a href=\"/#{project.path_with_namespace}/tree/#{@ref}/doc/api\">GitLab API doc directory</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
- it "should not handle malformed relative urls in reference links for a file in master" do
- actual = "[GitLab readme]: doc/api/README.md\n"
- expected = ""
- expect(markdown(actual)).to match(expected)
- end
- end
+ it "should not handle malformed relative urls in reference links for a file in master" do
+ actual = "[GitLab readme]: doc/api/README.md\n"
+ expected = ""
+ expect(markdown(actual)).to match(expected)
+ end
+ end
- describe 'markdown for empty repository' do
- before do
- @project = empty_project
- @repository = empty_project.repository
- end
+ context 'with an empty repository' do
+ before do
+ @project = create(:empty_project)
+ @repository = @project.repository
+ end
- it "should not touch relative urls" do
- actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n"
- expected = "<p><a href=\"doc/api/README.md\">GitLab API doc</a></p>\n"
- expect(markdown(actual)).to match(expected)
+ it "should not touch relative urls" do
+ actual = "[GitLab API doc][GitLab readme]\n [GitLab readme]: doc/api/README.md\n"
+ expected = "<p><a href=\"doc/api/README.md\">GitLab API doc</a></p>\n"
+ expect(markdown(actual)).to match(expected)
+ end
+ end
end
end
- describe "#render_wiki_content" do
+ describe '#render_wiki_content' do
before do
@wiki = double('WikiPage')
allow(@wiki).to receive(:content).and_return('wiki content')
@@ -765,103 +192,4 @@ describe GitlabMarkdownHelper do
helper.render_wiki_content(@wiki)
end
end
-
- describe '#gfm_with_tasks' do
- before(:all) do
- @source_text_asterisk = <<EOT.gsub(/^\s{8}/, '')
- * [ ] valid unchecked task
- * [x] valid lowercase checked task
- * [X] valid uppercase checked task
- * [ ] valid unchecked nested task
- * [x] valid checked nested task
-
- [ ] not an unchecked task - no list item
- [x] not a checked task - no list item
-
- * [ ] not an unchecked task - too many spaces
- * [x ] not a checked task - too many spaces
- * [] not an unchecked task - no spaces
- * Not a task [ ] - not at beginning
-EOT
-
- @source_text_dash = <<EOT.gsub(/^\s{8}/, '')
- - [ ] valid unchecked task
- - [x] valid lowercase checked task
- - [X] valid uppercase checked task
- - [ ] valid unchecked nested task
- - [x] valid checked nested task
-EOT
- end
-
- it 'should render checkboxes at beginning of asterisk list items' do
- rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
-
- expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
- expect(rendered_text).to match(
- /<input.*checkbox.*valid lowercase checked task/
- )
- expect(rendered_text).to match(
- /<input.*checkbox.*valid uppercase checked task/
- )
- end
-
- it 'should render checkboxes at beginning of dash list items' do
- rendered_text = markdown(@source_text_dash, parse_tasks: true)
-
- expect(rendered_text).to match(/<input.*checkbox.*valid unchecked task/)
- expect(rendered_text).to match(
- /<input.*checkbox.*valid lowercase checked task/
- )
- expect(rendered_text).to match(
- /<input.*checkbox.*valid uppercase checked task/
- )
- end
-
- it 'should not be confused by whitespace before bullets' do
- rendered_text_asterisk = markdown(@source_text_asterisk,
- parse_tasks: true)
- rendered_text_dash = markdown(@source_text_dash, parse_tasks: true)
-
- expect(rendered_text_asterisk).to match(
- /<input.*checkbox.*valid unchecked nested task/
- )
- expect(rendered_text_asterisk).to match(
- /<input.*checkbox.*valid checked nested task/
- )
- expect(rendered_text_dash).to match(
- /<input.*checkbox.*valid unchecked nested task/
- )
- expect(rendered_text_dash).to match(
- /<input.*checkbox.*valid checked nested task/
- )
- end
-
- it 'should not render checkboxes outside of list items' do
- rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
-
- expect(rendered_text).not_to match(
- /<input.*checkbox.*not an unchecked task - no list item/
- )
- expect(rendered_text).not_to match(
- /<input.*checkbox.*not a checked task - no list item/
- )
- end
-
- it 'should not render checkboxes with invalid formatting' do
- rendered_text = markdown(@source_text_asterisk, parse_tasks: true)
-
- expect(rendered_text).not_to match(
- /<input.*checkbox.*not an unchecked task - too many spaces/
- )
- expect(rendered_text).not_to match(
- /<input.*checkbox.*not a checked task - too many spaces/
- )
- expect(rendered_text).not_to match(
- /<input.*checkbox.*not an unchecked task - no spaces/
- )
- expect(rendered_text).not_to match(
- /Not a task.*<input.*checkbox.*not at beginning/
- )
- end
- end
end
diff --git a/spec/helpers/groups_helper.rb b/spec/helpers/groups_helper.rb
new file mode 100644
index 00000000000..3e99ab84ec9
--- /dev/null
+++ b/spec/helpers/groups_helper.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe GroupsHelper do
+ describe 'group_icon' do
+ avatar_file_path = File.join(Rails.root, 'public', 'gitlab_logo.png')
+
+ it 'should return an url for the avatar' do
+ group = create(:group)
+ group.avatar = File.open(avatar_file_path)
+ group.save!
+ expect(group_icon(group.path).to_s).
+ to match("/uploads/group/avatar/#{ group.id }/gitlab_logo.png")
+ end
+
+ it 'should give default avatar_icon when no avatar is present' do
+ group = create(:group)
+ group.save!
+ expect(group_icon(group.path)).to match('group_avatar.png')
+ end
+ end
+end
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/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 54dd8d4aa64..c08ddb4cae1 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -5,24 +5,6 @@ describe IssuesHelper do
let(:issue) { create :issue, project: project }
let(:ext_project) { create :redmine_project }
- describe "title_for_issue" do
- it "should return issue title if used internal tracker" do
- @project = project
- expect(title_for_issue(issue.iid)).to eq issue.title
- end
-
- it "should always return empty string if used external tracker" do
- @project = ext_project
- expect(title_for_issue(rand(100))).to eq ""
- end
-
- it "should always return empty string if project nil" do
- @project = nil
-
- expect(title_for_issue(rand(100))).to eq ""
- end
- end
-
describe "url_for_project_issues" do
let(:project_url) { ext_project.external_issue_tracker.project_url }
let(:ext_expected) do
diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb
index 1e64a201942..0b7e3b1d11f 100644
--- a/spec/helpers/labels_helper_spec.rb
+++ b/spec/helpers/labels_helper_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
describe LabelsHelper do
- it { expect(text_color_for_bg('#EEEEEE')).to eq('#333') }
- it { expect(text_color_for_bg('#222E2E')).to eq('#FFF') }
+ it { expect(text_color_for_bg('#EEEEEE')).to eq('#333333') }
+ it { expect(text_color_for_bg('#222E2E')).to eq('#FFFFFF') }
end
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index aef1108e333..e98b75afabc 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,46 @@ 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) }
+ let(:commit_id) { sample_commit[:id] }
+
+ before do
+ self.instance_variable_set(:@project, project)
+ end
+
+ it 'one level down' do
+ 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
+ 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
+ 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
+ result = relative_self_links('../foobar/baz/test.git', commit_id)
+ expect(result).to eq(["/baz/test", "/baz/test/tree/#{commit_id}"])
+ end
+
+ context 'personal project' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
+
+ it 'one level down with personal project' do
+ result = relative_self_links('../test.git', commit_id)
+ expect(result).to eq(["/#{user.username}/test", "/#{user.username}/test/tree/#{commit_id}"])
+ end
+ end
+ end
end
def stub_url(url)
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index 8271e00f41b..2013b3e4c2a 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -6,7 +6,7 @@ describe TreeHelper do
before {
@repository = project.repository
- @commit = project.repository.commit("e56497bb")
+ @commit = project.commit("e56497bb")
}
context "on a directory containing more than one file/directory" do
diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb
new file mode 100644
index 00000000000..3840e64981f
--- /dev/null
+++ b/spec/helpers/visibility_level_helper_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe VisibilityLevelHelper do
+ include Haml::Helpers
+
+ before :all do
+ init_haml_helpers
+ end
+
+ let(:project) { create(:project) }
+
+ describe 'visibility_level_description' do
+ shared_examples 'a visibility level description' do
+ let(:desc) do
+ visibility_level_description(Gitlab::VisibilityLevel::PRIVATE,
+ form_model)
+ end
+
+ let(:expected_class) do
+ class_name = case form_model.class.name
+ when 'String'
+ form_model
+ else
+ form_model.class.name
+ end
+
+ class_name.match(/(project|snippet)$/i)[0]
+ end
+
+ it 'should refer to the correct class' do
+ expect(desc).to match(/#{expected_class}/i)
+ end
+ end
+
+ context 'form_model argument is a String' do
+ context 'model object is a personal snippet' do
+ it_behaves_like 'a visibility level description' do
+ let(:form_model) { 'PersonalSnippet' }
+ end
+ end
+
+ context 'model object is a project snippet' do
+ it_behaves_like 'a visibility level description' do
+ let(:form_model) { 'ProjectSnippet' }
+ end
+ end
+
+ context 'model object is a project' do
+ it_behaves_like 'a visibility level description' do
+ let(:form_model) { 'Project' }
+ end
+ end
+ end
+
+ context 'form_model argument is a model object' do
+ context 'model object is a personal snippet' do
+ it_behaves_like 'a visibility level description' do
+ let(:form_model) { create(:personal_snippet) }
+ end
+ end
+
+ context 'model object is a project snippet' do
+ it_behaves_like 'a visibility level description' do
+ let(:form_model) { create(:project_snippet, project: project) }
+ end
+ end
+
+ context 'model object is a project' do
+ it_behaves_like 'a visibility level description' do
+ let(:form_model) { project }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/helpers/.gitkeep b/spec/javascripts/helpers/.gitkeep
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/spec/javascripts/helpers/.gitkeep
+++ /dev/null
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
new file mode 100644
index 00000000000..13b25862f57
--- /dev/null
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -0,0 +1,36 @@
+#= require jquery
+#= require jasmine-fixture
+#= require issue
+
+describe 'Issue', ->
+ describe 'task lists', ->
+ selectors = {
+ container: '.issue-details .description.js-task-list-container'
+ item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}'
+ textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}'
+ form: 'form.js-issue-update[action="/foo"]'
+ close: 'a.btn-close'
+ }
+
+ beforeEach ->
+ $container = affix(selectors.container)
+
+ # # These two elements are siblings inside the container
+ $container.find('.js-task-list-container').append(affix(selectors.item))
+ $container.find('.js-task-list-container').append(affix(selectors.textarea))
+
+ # Task lists don't get initialized unless this button exists. Not ideal.
+ $container.append(affix(selectors.close))
+
+ # This form is used to get the `update` URL. Not ideal.
+ $container.append(affix(selectors.form))
+
+ @issue = new Issue()
+
+ it 'submits an ajax request on tasklist:changed', ->
+ spyOn($, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PATCH')
+ expect(req.url).toBe('/foo')
+ expect(req.data.issue.description).not.toBe(null)
+
+ $('.js-task-list-field').trigger('tasklist:changed')
diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee
new file mode 100644
index 00000000000..3ebc4a4eed5
--- /dev/null
+++ b/spec/javascripts/merge_request_spec.js.coffee
@@ -0,0 +1,36 @@
+#= require jquery
+#= require jasmine-fixture
+#= require merge_request
+
+describe 'MergeRequest', ->
+ describe 'task lists', ->
+ selectors = {
+ container: '.merge-request-details .description.js-task-list-container'
+ item: '.wiki ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}'
+ textarea: '.wiki textarea.js-task-list-field{- [ ] Task List Item}'
+ form: 'form.js-merge-request-update[action="/foo"]'
+ close: 'a.btn-close'
+ }
+
+ beforeEach ->
+ $container = affix(selectors.container)
+
+ # # These two elements are siblings inside the container
+ $container.find('.js-task-list-container').append(affix(selectors.item))
+ $container.find('.js-task-list-container').append(affix(selectors.textarea))
+
+ # Task lists don't get initialized unless this button exists. Not ideal.
+ $container.append(affix(selectors.close))
+
+ # This form is used to get the `update` URL. Not ideal.
+ $container.append(affix(selectors.form))
+
+ @merge = new MergeRequest({})
+
+ it 'submits an ajax request on tasklist:changed', ->
+ spyOn($, 'ajax').and.callFake (req) ->
+ expect(req.type).toBe('PATCH')
+ expect(req.url).toBe('/foo')
+ expect(req.data.merge_request.description).not.toBe(null)
+
+ $('.js-task-list-field').trigger('tasklist:changed')
diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee
new file mode 100644
index 00000000000..de2e8e7f6c8
--- /dev/null
+++ b/spec/javascripts/notes_spec.js.coffee
@@ -0,0 +1,30 @@
+#= require jquery
+#= require jasmine-fixture
+#= require notes
+
+window.gon = {}
+window.disableButtonIfEmptyField = -> null
+
+describe 'Notes', ->
+ describe 'task lists', ->
+ selectors = {
+ container: 'li.note .js-task-list-container'
+ item: '.note-text ul.task-list li.task-list-item input.task-list-item-checkbox[type=checkbox] {Task List Item}'
+ textarea: '.note-edit-form form textarea.js-task-list-field{- [ ] Task List Item}'
+ }
+
+ beforeEach ->
+ $container = affix(selectors.container)
+
+ # These two elements are siblings inside the container
+ $container.find('.js-task-list-container').append(affix(selectors.item))
+ $container.find('.js-task-list-container').append(affix(selectors.textarea))
+
+ @notes = new Notes()
+
+ it 'submits the form on tasklist:changed', ->
+ submitted = false
+ $('form').on 'submit', (e) -> submitted = true; e.preventDefault()
+
+ $('.js-task-list-field').trigger('tasklist:changed')
+ expect(submitted).toBe(true)
diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee
new file mode 100644
index 00000000000..57dcc2161d3
--- /dev/null
+++ b/spec/javascripts/shortcuts_issuable_spec.js.coffee
@@ -0,0 +1,83 @@
+#= require jquery
+#= require jasmine-fixture
+
+#= require shortcuts_issuable
+
+describe 'ShortcutsIssuable', ->
+ beforeEach ->
+ @shortcut = new ShortcutsIssuable()
+
+ describe '#replyWithSelectedText', ->
+ # Stub window.getSelection to return the provided String.
+ stubSelection = (text) ->
+ window.getSelection = -> text
+
+ beforeEach ->
+ @selector = 'form.js-main-target-form textarea#note_note'
+ affix(@selector)
+
+ describe 'with empty selection', ->
+ it 'does nothing', ->
+ stubSelection('')
+ @shortcut.replyWithSelectedText()
+ expect($(@selector).val()).toBe('')
+
+ describe 'with any selection', ->
+ beforeEach ->
+ stubSelection('Selected text.')
+
+ it 'leaves existing input intact', ->
+ $(@selector).val('This text was already here.')
+ expect($(@selector).val()).toBe('This text was already here.')
+
+ @shortcut.replyWithSelectedText()
+ expect($(@selector).val()).
+ toBe("This text was already here.\n> Selected text.\n\n")
+
+ it 'triggers `input`', ->
+ triggered = false
+ $(@selector).on 'input', -> triggered = true
+ @shortcut.replyWithSelectedText()
+
+ expect(triggered).toBe(true)
+
+ it 'triggers `focus`', ->
+ focused = false
+ $(@selector).on 'focus', -> focused = true
+ @shortcut.replyWithSelectedText()
+
+ expect(focused).toBe(true)
+
+ describe 'with a one-line selection', ->
+ it 'quotes the selection', ->
+ stubSelection('This text has been selected.')
+
+ @shortcut.replyWithSelectedText()
+
+ expect($(@selector).val()).
+ toBe("> This text has been selected.\n\n")
+
+ describe 'with a multi-line selection', ->
+ it 'quotes the selected lines as a group', ->
+ stubSelection(
+ """
+ Selected line one.
+
+ Selected line two.
+ Selected line three.
+
+ """
+ )
+
+ @shortcut.replyWithSelectedText()
+
+ expect($(@selector).val()).
+ toBe(
+ """
+ > Selected line one.
+ > Selected line two.
+ > Selected line three.
+
+
+ """
+ )
diff --git a/spec/javascripts/stat_graph_contributors_graph_spec.js b/spec/javascripts/stat_graph_contributors_graph_spec.js
index 1090cb7f620..78d39f1b428 100644
--- a/spec/javascripts/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/stat_graph_contributors_graph_spec.js
@@ -1,3 +1,5 @@
+//= require stat_graph_contributors_graph
+
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
it("set the x_domain", function () {
diff --git a/spec/javascripts/stat_graph_contributors_util_spec.js b/spec/javascripts/stat_graph_contributors_util_spec.js
index 9c1b588861d..ee90892eb48 100644
--- a/spec/javascripts/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/stat_graph_contributors_util_spec.js
@@ -1,3 +1,5 @@
+//= require stat_graph_contributors_util
+
describe("ContributorsStatGraphUtil", function () {
describe("#parse_log", function () {
diff --git a/spec/javascripts/stat_graph_spec.js b/spec/javascripts/stat_graph_spec.js
index b589af34610..4c652910cd6 100644
--- a/spec/javascripts/stat_graph_spec.js
+++ b/spec/javascripts/stat_graph_spec.js
@@ -1,3 +1,5 @@
+//= require stat_graph
+
describe("StatGraph", function () {
describe("#get_log", function () {
diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml
index 9bfa261a356..168c9618643 100644
--- a/spec/javascripts/support/jasmine.yml
+++ b/spec/javascripts/support/jasmine.yml
@@ -1,76 +1,15 @@
-# src_files
+# path to parent directory of spec_files
+# relative path from Rails.root
#
-# Return an array of filepaths relative to src_dir to include before jasmine specs.
-# Default: []
+# Alternatively accept an array of directory to include external spec files
+# spec_dir:
+# - spec/javascripts
+# - ../engine/spec/javascripts
#
-# EXAMPLE:
-#
-# src_files:
-# - lib/source1.js
-# - lib/source2.js
-# - dist/**/*.js
-#
-src_files:
- - assets/application.js
-
-# stylesheets
-#
-# Return an array of stylesheet filepaths relative to src_dir to include before jasmine specs.
-# Default: []
-#
-# EXAMPLE:
-#
-# stylesheets:
-# - css/style.css
-# - stylesheets/*.css
-#
-stylesheets:
- - stylesheets/**/*.css
-
-# helpers
-#
-# Return an array of filepaths relative to spec_dir to include before jasmine specs.
-# Default: ["helpers/**/*.js"]
-#
-# EXAMPLE:
-#
-# helpers:
-# - helpers/**/*.js
-#
-helpers:
- - helpers/**/*.js
+# defaults to spec/javascripts
+spec_dir: spec/javascripts
-# spec_files
-#
-# Return an array of filepaths relative to spec_dir to include.
-# Default: ["**/*[sS]pec.js"]
-#
-# EXAMPLE:
-#
-# spec_files:
-# - **/*[sS]pec.js
-#
+# list of file expressions to include as specs into spec runner
+# relative path from spec_dir
spec_files:
- - '**/*[sS]pec.js'
-
-# src_dir
-#
-# Source directory path. Your src_files must be returned relative to this path. Will use root if left blank.
-# Default: project root
-#
-# EXAMPLE:
-#
-# src_dir: public
-#
-src_dir:
-
-# spec_dir
-#
-# Spec directory path. Your spec_files must be returned relative to this path.
-# Default: spec/javascripts
-#
-# EXAMPLE:
-#
-# spec_dir: spec/javascripts
-#
-spec_dir: spec/javascripts
+ - "**/*[Ss]pec.{js.coffee,js,coffee}"
diff --git a/spec/javascripts/support/jasmine_helper.rb b/spec/javascripts/support/jasmine_helper.rb
index b4919802afe..4d73aec5a31 100644
--- a/spec/javascripts/support/jasmine_helper.rb
+++ b/spec/javascripts/support/jasmine_helper.rb
@@ -8,4 +8,8 @@
# config.boot_files = lambda { ['/absolute/path/to/boot_dir/file.js'] }
#end
#
-
+#Example: prevent PhantomJS auto install, uses PhantomJS already on your path.
+#Jasmine.configure do |config|
+# config.prevent_phantom_js_auto_install = true
+#end
+#
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index ac602eac154..05bcebaa3a2 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
describe ExtractsPath do
include ExtractsPath
+ include RepoHelpers
+ include Rails.application.routes.url_helpers
let(:project) { double('project') }
@@ -11,6 +13,20 @@ describe ExtractsPath do
project.stub(path_with_namespace: 'gitlab/gitlab-ci')
end
+ describe '#assign_ref' do
+ let(:ref) { sample_commit[:id] }
+ let(:params) { {path: sample_commit[:line_code_path], ref: ref} }
+
+ before do
+ @project = create(:project)
+ end
+
+ it "log tree path should have no escape sequences" do
+ assign_ref_vars
+ expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
+ end
+ end
+
describe '#extract_ref' do
it "returns an empty pair when no @project is set" do
@project = nil
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/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
index 768312f0028..d0aad54f677 100644
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ b/spec/lib/gitlab/backend/grack_auth_spec.rb
@@ -6,7 +6,7 @@ describe Grack::Auth do
let(:app) { lambda { |env| [200, {}, "Success!"] } }
let!(:auth) { Grack::Auth.new(app) }
- let(:env) {
+ let(:env) {
{
"rack.input" => "",
"REQUEST_METHOD" => "GET",
@@ -85,6 +85,17 @@ describe Grack::Auth do
it "responds with status 401" do
expect(status).to eq(401)
end
+
+ context "when the user is IP banned" do
+ before do
+ expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
+ end
+
+ it "responds with status 401" do
+ expect(status).to eq(401)
+ end
+ end
end
context "when authentication succeeds" do
@@ -109,10 +120,49 @@ describe Grack::Auth do
end
context "when the user isn't blocked" do
+ before do
+ expect(Rack::Attack::Allow2Ban).to receive(:reset)
+ end
+
it "responds with status 200" do
expect(status).to eq(200)
end
end
+
+ context "when blank password attempts follow a valid login" do
+ let(:options) { Gitlab.config.rack_attack.git_basic_auth }
+ let(:maxretry) { options[:maxretry] - 1 }
+ let(:ip) { '1.2.3.4' }
+
+ before do
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+ Rack::Attack::Allow2Ban.reset(ip, options)
+ end
+
+ after do
+ Rack::Attack::Allow2Ban.reset(ip, options)
+ end
+
+ def attempt_login(include_password)
+ password = include_password ? user.password : ""
+ env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, password)
+ Grack::Auth.new(app)
+ auth.call(env).first
+ end
+
+ it "repeated attempts followed by successful attempt" do
+ for n in 0..maxretry do
+ expect(attempt_login(false)).to eq(401)
+ end
+
+ expect(attempt_login(true)).to eq(200)
+ expect(Rack::Attack::Allow2Ban.send(:banned?, ip)).to eq(nil)
+
+ for n in 0..maxretry do
+ expect(attempt_login(false)).to eq(401)
+ end
+ end
+ end
end
context "when the user doesn't have access to the project" do
diff --git a/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb b/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb
new file mode 100644
index 00000000000..2ac496fd669
--- /dev/null
+++ b/spec/lib/gitlab/backend/rack_attack_helpers_spec.rb
@@ -0,0 +1,35 @@
+require "spec_helper"
+
+describe 'RackAttackHelpers' do
+ describe 'reset' do
+ let(:discriminator) { 'test-key'}
+ let(:maxretry) { 5 }
+ let(:period) { 1.minute }
+ let(:options) { { findtime: period, bantime: 60, maxretry: maxretry } }
+
+ def do_filter
+ for i in 1..maxretry - 1 do
+ status = Rack::Attack::Allow2Ban.filter(discriminator, options) { true }
+ expect(status).to eq(false)
+ end
+ end
+
+ def do_reset
+ Rack::Attack::Allow2Ban.reset(discriminator, options)
+ end
+
+ before do
+ do_reset
+ end
+
+ after do
+ do_reset
+ end
+
+ it 'user is not banned after n - 1 retries' do
+ do_filter
+ do_reset
+ do_filter
+ end
+ end
+end
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
new file mode 100644
index 00000000000..dd450e9967b
--- /dev/null
+++ b/spec/lib/gitlab/bitbucket_import/client_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::BitbucketImport::Client do
+ let(:token) { '123456' }
+ let(:secret) { 'secret' }
+ let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
+
+ before do
+ Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket")
+ end
+
+ it 'all OAuth client options are symbols' do
+ client.consumer.options.keys.each do |key|
+ expect(key).to be_kind_of(Symbol)
+ 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/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 40eb45e37ca..8b7946f3117 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Diff::File do
include RepoHelpers
let(:project) { create(:project) }
- let(:commit) { project.repository.commit(sample_commit.id) }
+ let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.diffs.first }
let(:diff_file) { Gitlab::Diff::File.new(diff) }
diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb
index 918f6d0ead4..4d5d1431683 100644
--- a/spec/lib/gitlab/diff/parser_spec.rb
+++ b/spec/lib/gitlab/diff/parser_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Diff::Parser do
include RepoHelpers
let(:project) { create(:project) }
- let(:commit) { project.repository.commit(sample_commit.id) }
+ let(:commit) { project.commit(sample_commit.id) }
let(:diff) { commit.diffs.first }
let(:parser) { Gitlab::Diff::Parser.new }
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 666398eedd4..39be9d64644 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1,25 +1,26 @@
require 'spec_helper'
describe Gitlab::GitAccess do
- let(:access) { Gitlab::GitAccess.new }
+ let(:access) { Gitlab::GitAccess.new(actor, project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:actor) { user }
describe 'can_push_to_branch?' do
describe 'push to none protected branch' do
it "returns true if user is a master" do
project.team << [user, :master]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_truthy
+ expect(access.can_push_to_branch?("random_branch")).to be_truthy
end
it "returns true if user is a developer" do
project.team << [user, :developer]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_truthy
+ expect(access.can_push_to_branch?("random_branch")).to be_truthy
end
it "returns false if user is a reporter" do
project.team << [user, :reporter]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, "random_branch")).to be_falsey
+ expect(access.can_push_to_branch?("random_branch")).to be_falsey
end
end
@@ -30,17 +31,17 @@ describe Gitlab::GitAccess do
it "returns true if user is a master" do
project.team << [user, :master]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
end
it "returns false if user is a developer" do
project.team << [user, :developer]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey
+ expect(access.can_push_to_branch?(@branch.name)).to be_falsey
end
it "returns false if user is a reporter" do
project.team << [user, :reporter]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey
+ expect(access.can_push_to_branch?(@branch.name)).to be_falsey
end
end
@@ -51,17 +52,17 @@ describe Gitlab::GitAccess do
it "returns true if user is a master" do
project.team << [user, :master]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
end
it "returns true if user is a developer" do
project.team << [user, :developer]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_truthy
+ expect(access.can_push_to_branch?(@branch.name)).to be_truthy
end
it "returns false if user is a reporter" do
project.team << [user, :reporter]
- expect(Gitlab::GitAccess.can_push_to_branch?(user, project, @branch.name)).to be_falsey
+ expect(access.can_push_to_branch?(@branch.name)).to be_falsey
end
end
@@ -72,7 +73,7 @@ describe Gitlab::GitAccess do
before { project.team << [user, :master] }
context 'pull code' do
- subject { access.download_access_check(user, project) }
+ subject { access.download_access_check }
it { expect(subject.allowed?).to be_truthy }
end
@@ -82,7 +83,7 @@ describe Gitlab::GitAccess do
before { project.team << [user, :guest] }
context 'pull code' do
- subject { access.download_access_check(user, project) }
+ subject { access.download_access_check }
it { expect(subject.allowed?).to be_falsey }
end
@@ -95,7 +96,7 @@ describe Gitlab::GitAccess do
end
context 'pull code' do
- subject { access.download_access_check(user, project) }
+ subject { access.download_access_check }
it { expect(subject.allowed?).to be_falsey }
end
@@ -103,7 +104,7 @@ describe Gitlab::GitAccess do
describe 'without acccess to project' do
context 'pull code' do
- subject { access.download_access_check(user, project) }
+ subject { access.download_access_check }
it { expect(subject.allowed?).to be_falsey }
end
@@ -111,17 +112,18 @@ describe Gitlab::GitAccess do
describe 'deploy key permissions' do
let(:key) { create(:deploy_key) }
+ let(:actor) { key }
context 'pull code' do
context 'allowed' do
before { key.projects << project }
- subject { access.download_access_check(key, project) }
+ subject { access.download_access_check }
it { expect(subject.allowed?).to be_truthy }
end
context 'denied' do
- subject { access.download_access_check(key, project) }
+ subject { access.download_access_check }
it { expect(subject.allowed?).to be_falsey }
end
@@ -205,7 +207,7 @@ describe Gitlab::GitAccess do
permissions_matrix[role].each do |action, allowed|
context action do
- subject { access.push_access_check(user, project, changes[action]) }
+ subject { access.push_access_check(changes[action]) }
it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
end
@@ -221,7 +223,7 @@ describe Gitlab::GitAccess do
updated_permissions_matrix[role].each do |action, allowed|
context action do
- subject { access.push_access_check(user, project, changes[action]) }
+ subject { access.push_access_check(changes[action]) }
it { expect(subject.allowed?).to allowed ? be_truthy : be_falsey }
end
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index c31c6764091..4cb91094cb3 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Gitlab::GitAccessWiki do
- let(:access) { Gitlab::GitAccessWiki.new }
+ let(:access) { Gitlab::GitAccessWiki.new(user, project) }
let(:project) { create(:project) }
let(:user) { create(:user) }
@@ -11,7 +11,7 @@ describe Gitlab::GitAccessWiki do
project.team << [user, :developer]
end
- subject { access.push_access_check(user, project, changes) }
+ subject { access.push_access_check(changes) }
it { expect(subject.allowed?).to be_truthy }
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
new file mode 100644
index 00000000000..26618120316
--- /dev/null
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Client do
+ let(:token) { '123456' }
+ let(:client) { Gitlab::GithubImport::Client.new(token) }
+
+ before do
+ Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "github")
+ end
+
+ it 'all OAuth2 client options are symbols' do
+ client.client.options.keys.each do |key|
+ expect(key).to be_kind_of(Symbol)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index 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/client_spec.rb b/spec/lib/gitlab/gitlab_import/client_spec.rb
new file mode 100644
index 00000000000..c511c515474
--- /dev/null
+++ b/spec/lib/gitlab/gitlab_import/client_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::GitlabImport::Client do
+ let(:token) { '123456' }
+ let(:client) { Gitlab::GitlabImport::Client.new(token) }
+
+ before do
+ Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "gitlab")
+ end
+
+ it 'all OAuth2 client options are symbols' do
+ client.client.options.keys.each do |key|
+ expect(key).to be_kind_of(Symbol)
+ end
+ end
+end
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..a66b811e0fd
--- /dev/null
+++ b/spec/lib/gitlab/google_code_import/client_spec.rb
@@ -0,0 +1,35 @@
+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)
+ expect(subject.incompatible_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..67378328336
--- /dev/null
+++ b/spec/lib/gitlab/google_code_import/importer_spec.rb
@@ -0,0 +1,85 @@
+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("Google Code")
+ 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_task+prev_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/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb
index a2b05249147..707a0521ab3 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/ldap/access_spec.rb
@@ -20,12 +20,26 @@ describe Gitlab::LDAP::Access do
before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: true) }
it { is_expected.to be_falsey }
+
+ it "should block user in GitLab" do
+ access.allowed?
+ user.should be_blocked
+ end
end
context 'and has no disabled flag in active diretory' do
- before { Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false) }
+ before do
+ user.block
+
+ Gitlab::LDAP::Person.stub(disabled_via_active_directory?: false)
+ end
it { is_expected.to be_truthy }
+
+ it "should unblock user in GitLab" do
+ access.allowed?
+ user.should_not be_blocked
+ end
end
context 'without ActiveDirectory enabled' do
@@ -38,4 +52,4 @@ describe Gitlab::LDAP::Access do
end
end
end
-end \ No newline at end of file
+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/markdown/autolink_filter_spec.rb b/spec/lib/gitlab/markdown/autolink_filter_spec.rb
new file mode 100644
index 00000000000..0bbdc11a979
--- /dev/null
+++ b/spec/lib/gitlab/markdown/autolink_filter_spec.rb
@@ -0,0 +1,106 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe AutolinkFilter do
+ let(:link) { 'http://about.gitlab.com/' }
+
+ def filter(html, options = {})
+ described_class.call(html, options)
+ end
+
+ it 'does nothing when :autolink is false' do
+ exp = act = link
+ expect(filter(act, autolink: false).to_html).to eq exp
+ end
+
+ it 'does nothing with non-link text' do
+ exp = act = 'This text contains no links to autolink'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ context 'Rinku schemes' do
+ it 'autolinks http' do
+ doc = filter("See #{link}")
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks https' do
+ link = 'https://google.com/'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks ftp' do
+ link = 'ftp://ftp.us.debian.org/debian/'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks short URLs' do
+ link = 'http://localhost:3000/'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'accepts link_attr options' do
+ doc = filter("See #{link}", link_attr: {class: 'custom'})
+
+ expect(doc.at_css('a')['class']).to eq 'custom'
+ end
+
+ described_class::IGNORE_PARENTS.each do |elem|
+ it "ignores valid links contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>See #{link}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+
+ context 'other schemes' do
+ let(:link) { 'foo://bar.baz/' }
+
+ it 'autolinks smb' do
+ link = 'smb:///Volumes/shared/foo.pdf'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'autolinks irc' do
+ link = 'irc://irc.freenode.net/git'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq link
+ expect(doc.at_css('a')['href']).to eq link
+ end
+
+ it 'does not include trailing punctuation' do
+ doc = filter("See #{link}.")
+ expect(doc.at_css('a').text).to eq link
+
+ doc = filter("See #{link}, ok?")
+ expect(doc.at_css('a').text).to eq link
+ end
+
+ it 'accepts link_attr options' do
+ doc = filter("See #{link}", link_attr: {class: 'custom'})
+ expect(doc.at_css('a')['class']).to eq 'custom'
+ end
+
+ described_class::IGNORE_PARENTS.each do |elem|
+ it "ignores valid links contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>See #{link}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
new file mode 100644
index 00000000000..7274cb309a0
--- /dev/null
+++ b/spec/lib/gitlab/markdown/commit_range_reference_filter_spec.rb
@@ -0,0 +1,144 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe CommitRangeReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:commit1) { project.commit }
+ let(:commit2) { project.commit("HEAD~2") }
+
+ it 'requires project context' do
+ expect { described_class.call('Commit Range 1c002d..d200c1', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Commit Range #{commit1.id}..#{commit2.id}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { "#{commit1.id}...#{commit2.id}" }
+ let(:reference2) { "#{commit1.id}..#{commit2.id}" }
+
+ it 'links to a valid two-dot reference' do
+ doc = filter("See #{reference2}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project.namespace, project, from: "#{commit1.id}^", to: commit2.id)
+ end
+
+ it 'links to a valid three-dot reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id)
+ end
+
+ it 'links to a valid short ID' do
+ reference = "#{commit1.short_id}...#{commit2.id}"
+ reference2 = "#{commit1.id}...#{commit2.short_id}"
+
+ exp = commit1.short_id + '...' + commit2.short_id
+
+ expect(filter("See #{reference}").css('a').first.text).to eq exp
+ expect(filter("See #{reference2}").css('a').first.text).to eq exp
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("See (#{reference}.)")
+
+ exp = Regexp.escape("#{commit1.short_id}...#{commit2.short_id}")
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs' do
+ exp = act = "See #{commit1.id.reverse}...#{commit2.id}"
+
+ expect(project).to receive(:valid_repo?).and_return(true)
+ expect(project.repository).to receive(:commit).with(commit1.id.reverse)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Commits #{commit1.id} through #{commit2.id}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit_range'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("See #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path option' do
+ doc = filter("See #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_compare_url(project.namespace, project, from: commit1.id, to: commit2.id, only_path: true)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("See #{reference}")
+ expect(result[:references][:commit_range]).not_to be_empty
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:commit1) { project.commit }
+ let(:commit2) { project.commit("HEAD~2") }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit1.id}...#{commit2.id}" }
+
+ context 'when user can access reference' do
+ before { allow_cross_reference! }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_compare_url(project2.namespace, project2, from: commit1.id, to: commit2.id)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+
+ exp = Regexp.escape("#{project2.path_with_namespace}@#{commit1.short_id}...#{commit2.short_id}")
+ expect(doc.to_html).to match(/\(<a.+>#{exp}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id.reverse}...#{commit2.id}"
+ expect(filter(act).to_html).to eq exp
+
+ exp = act = "Fixed #{project2.path_with_namespace}##{commit1.id}...#{commit2.id.reverse}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("See #{reference}")
+ expect(result[:references][:commit_range]).not_to be_empty
+ end
+ end
+
+ context 'when user cannot access reference' do
+ before { disallow_cross_reference! }
+
+ it 'ignores valid references' do
+ exp = act = "See #{reference}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
new file mode 100644
index 00000000000..cc32a4fcf03
--- /dev/null
+++ b/spec/lib/gitlab/markdown/commit_reference_filter_spec.rb
@@ -0,0 +1,138 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe CommitReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:commit) { project.commit }
+
+ it 'requires project context' do
+ expect { described_class.call('Commit 1c002d', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Commit #{commit.id}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { commit.id }
+
+ # Let's test a variety of commit SHA sizes just to be paranoid
+ [6, 8, 12, 18, 20, 32, 40].each do |size|
+ it "links to a valid reference of #{size} characters" do
+ doc = filter("See #{reference[0...size]}")
+
+ expect(doc.css('a').first.text).to eq commit.short_id
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project.namespace, project, reference)
+ end
+ end
+
+ it 'always uses the short ID as the link text' do
+ doc = filter("See #{commit.id}")
+ expect(doc.text).to eq "See #{commit.short_id}"
+
+ doc = filter("See #{commit.id[0...6]}")
+ expect(doc.text).to eq "See #{commit.short_id}"
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{commit.short_id}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs' do
+ exp = act = "See #{reference.reverse}"
+
+ expect(project).to receive(:valid_repo?).and_return(true)
+ expect(project.repository).to receive(:commit).with(reference.reverse)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq commit.link_title
+ end
+
+ it 'escapes the title attribute' do
+ allow_any_instance_of(Commit).to receive(:title).and_return(%{"></a>whatever<a title="})
+
+ doc = filter("See #{reference}")
+ expect(doc.text).to eq "See #{commit.short_id}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("See #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-commit'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("See #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("See #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_commit_url(project.namespace, project, reference, only_path: true)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("See #{reference}")
+ expect(result[:references][:commit]).not_to be_empty
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:commit) { project.commit }
+ let(:reference) { "#{project2.path_with_namespace}@#{commit.id}" }
+
+ context 'when user can access reference' do
+ before { allow_cross_reference! }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_commit_url(project2.namespace, project2, commit.id)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+
+ exp = Regexp.escape(project2.path_with_namespace)
+ expect(doc.to_html).to match(/\(<a.+>#{exp}@#{commit.short_id}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid commit IDs on the referenced project' do
+ exp = act = "Committed #{project2.path_with_namespace}##{commit.id.reverse}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("See #{reference}")
+ expect(result[:references][:commit]).not_to be_empty
+ end
+ end
+
+ context 'when user cannot access reference' do
+ before { disallow_cross_reference! }
+
+ it 'ignores valid references' do
+ exp = act = "See #{reference}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/cross_project_reference_spec.rb b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
new file mode 100644
index 00000000000..4698d6138c2
--- /dev/null
+++ b/spec/lib/gitlab/markdown/cross_project_reference_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe CrossProjectReference do
+ # context in the html-pipeline sense, not in the rspec sense
+ let(:context) do
+ {
+ current_user: double('user'),
+ project: double('project')
+ }
+ end
+
+ include described_class
+
+ describe '#project_from_ref' do
+ context 'when no project was referenced' do
+ it 'returns the project from context' do
+ expect(project_from_ref(nil)).to eq context[:project]
+ end
+ end
+
+ context 'when referenced project does not exist' do
+ it 'returns nil' do
+ expect(project_from_ref('invalid/reference')).to be_nil
+ end
+ end
+
+ context 'when referenced project exists' do
+ let(:project2) { double('referenced project') }
+
+ before do
+ expect(Project).to receive(:find_with_namespace).
+ with('cross/reference').and_return(project2)
+ end
+
+ context 'and the user has permission to read it' do
+ it 'returns the referenced project' do
+ expect(self).to receive(:user_can_reference_project?).
+ with(project2).and_return(true)
+
+ expect(project_from_ref('cross/reference')).to eq project2
+ end
+ end
+
+ context 'and the user does not have permission to read it' do
+ it 'returns nil' do
+ expect(self).to receive(:user_can_reference_project?).
+ with(project2).and_return(false)
+
+ expect(project_from_ref('cross/reference')).to be_nil
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/emoji_filter_spec.rb b/spec/lib/gitlab/markdown/emoji_filter_spec.rb
new file mode 100644
index 00000000000..18d55c4818f
--- /dev/null
+++ b/spec/lib/gitlab/markdown/emoji_filter_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe EmojiFilter do
+ def filter(html, contexts = {})
+ described_class.call(html, contexts)
+ end
+
+ before do
+ ActionController::Base.asset_host = 'https://foo.com'
+ end
+
+ it 'replaces supported emoji' do
+ doc = filter('<p>:heart:</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png'
+ end
+
+ it 'ignores unsupported emoji' do
+ exp = act = '<p>:foo:</p>'
+ doc = filter(act)
+ expect(doc.to_html).to match Regexp.escape(exp)
+ end
+
+ it 'correctly encodes the URL' do
+ doc = filter('<p>:+1:</p>')
+ expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png'
+ end
+
+ it 'matches at the start of a string' do
+ doc = filter(':+1:')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches at the end of a string' do
+ doc = filter('This gets a :-1:')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches with adjacent text' do
+ doc = filter('+1 (:+1:)')
+ expect(doc.css('img').size).to eq 1
+ end
+
+ it 'matches multiple emoji in a row' do
+ doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
+ expect(doc.css('img').size).to eq 3
+ end
+
+ it 'has a title attribute' do
+ doc = filter(':-1:')
+ expect(doc.css('img').first.attr('title')).to eq ':-1:'
+ end
+
+ it 'has an alt attribute' do
+ doc = filter(':-1:')
+ expect(doc.css('img').first.attr('alt')).to eq ':-1:'
+ end
+
+ it 'has an align attribute' do
+ doc = filter(':8ball:')
+ expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
+ end
+
+ it 'has an emoji class' do
+ doc = filter(':cat:')
+ expect(doc.css('img').first.attr('class')).to eq 'emoji'
+ end
+
+ it 'has height and width attributes' do
+ doc = filter(':dog:')
+ img = doc.css('img').first
+
+ expect(img.attr('width')).to eq '20'
+ expect(img.attr('height')).to eq '20'
+ end
+
+ it 'keeps whitespace intact' do
+ doc = filter('This deserves a :+1:, big time.')
+
+ expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/)
+ end
+
+ it 'uses a custom asset_root context' do
+ root = Gitlab.config.gitlab.url + 'gitlab/root'
+
+ doc = filter(':smile:', asset_root: root)
+ expect(doc.css('img').first.attr('src')).to start_with(root)
+ end
+
+ it 'uses a custom asset_host context' do
+ ActionController::Base.asset_host = 'https://cdn.example.com'
+
+ doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
+ expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
new file mode 100644
index 00000000000..b19bc125b92
--- /dev/null
+++ b/spec/lib/gitlab/markdown/external_issue_reference_filter_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe ExternalIssueReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ def helper
+ IssuesHelper
+ end
+
+ let(:project) { create(:jira_project) }
+ let(:issue) { double('issue', iid: 123) }
+
+ context 'JIRA issue references' do
+ let(:reference) { "JIRA-#{issue.iid}" }
+
+ it 'requires project context' do
+ expect { described_class.call('Issue JIRA-123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue JIRA-#{issue.iid}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'ignores valid references when using default tracker' do
+ expect(project).to receive(:default_issues_tracker?).and_return(true)
+
+ exp = act = "Issue #{reference}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('href'))
+ .to eq helper.url_for_issue(reference, project)
+ end
+
+ it 'links to the external tracker' do
+ doc = filter("Issue #{reference}")
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq "http://jira.example/browse/#{reference}"
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Issue (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{reference}<\/a>\.\)/)
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Issue in JIRA tracker"
+ end
+
+ it 'escapes the title attribute' do
+ allow(project.external_issue_tracker).to receive(:title).
+ and_return(%{"></a>whatever<a title="})
+
+ doc = filter("Issue #{reference}")
+ expect(doc.text).to eq "Issue #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Issue #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Issue #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).to eq helper.url_for_issue("#{reference}", project, only_path: true)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
new file mode 100644
index 00000000000..08382b3e7e8
--- /dev/null
+++ b/spec/lib/gitlab/markdown/issue_reference_filter_spec.rb
@@ -0,0 +1,143 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe IssueReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ def helper
+ IssuesHelper
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('Issue #123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Issue ##{issue.iid}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { "##{issue.iid}" }
+
+ it 'ignores valid references when using non-default tracker' do
+ expect(project).to receive(:get_issue).with(issue.iid).and_return(nil)
+
+ exp = act = "Issue ##{issue.iid}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("Fixed #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs' do
+ exp = act = "Fixed ##{issue.iid + 1}"
+
+ expect(project).to receive(:get_issue).with(issue.iid + 1).and_return(nil)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Issue: #{issue.title}"
+ end
+
+ it 'escapes the title attribute' do
+ issue.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = filter("Issue #{reference}")
+ expect(doc.text).to eq "Issue #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Issue #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-issue'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Issue #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Issue #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq helper.url_for_issue(issue.iid, project, only_path: true)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Fixed #{reference}")
+ expect(result[:references][:issue]).to eq [issue]
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:issue) { create(:issue, project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}##{issue.iid}" }
+
+ context 'when user can access reference' do
+ before { allow_cross_reference! }
+
+ it 'ignores valid references when cross-reference project uses external tracker' do
+ expect_any_instance_of(Project).to receive(:get_issue).
+ with(issue.iid).and_return(nil)
+
+ exp = act = "Issue ##{issue.iid}"
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq helper.url_for_issue(issue.iid, project2)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Fixed (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid issue IDs on the referenced project' do
+ exp = act = "Fixed #{project2.path_with_namespace}##{issue.iid + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Fixed #{reference}")
+ expect(result[:references][:issue]).to eq [issue]
+ end
+ end
+
+ context 'when user cannot access reference' do
+ before { disallow_cross_reference! }
+
+ it 'ignores valid references' do
+ exp = act = "See #{reference}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/label_reference_filter_spec.rb b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
new file mode 100644
index 00000000000..9f898837466
--- /dev/null
+++ b/spec/lib/gitlab/markdown/label_reference_filter_spec.rb
@@ -0,0 +1,153 @@
+require 'spec_helper'
+require 'html/pipeline'
+
+module Gitlab::Markdown
+ describe LabelReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:empty_project) }
+ let(:label) { create(:label, project: project) }
+ let(:reference) { "~#{label.id}" }
+
+ it 'requires project context' do
+ expect { described_class.call('Label ~123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Label #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ it 'includes default classes' do
+ doc = filter("Label #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-label'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Label #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Label #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_issues_url(project.namespace, project, label_name: label.name, only_path: true)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Label #{reference}")
+ expect(result[:references][:label]).to eq [label]
+ end
+
+ describe 'label span element' do
+ it 'includes default classes' do
+ doc = filter("Label #{reference}")
+ expect(doc.css('a span').first.attr('class')).to eq 'label color-label'
+ end
+
+ it 'includes a style attribute' do
+ doc = filter("Label #{reference}")
+ expect(doc.css('a span').first.attr('style')).to match(/\Abackground-color: #\h{6}; color: #\h{6}\z/)
+ end
+ end
+
+ context 'Integer-based references' do
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label IDs' do
+ exp = act = "Label ~#{label.id + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'String-based single-word references' do
+ let(:label) { create(:label, name: 'gfm', project: project) }
+ let(:reference) { "~#{label.name}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See gfm'
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label ~#{label.name.reverse}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'String-based multi-word references in quotes' do
+ let(:label) { create(:label, name: 'gfm references', project: project) }
+
+ context 'in single quotes' do
+ let(:reference) { "~'#{label.name}'" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = "Label ~'#{label.name.reverse}'"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'in double quotes' do
+ let(:reference) { %(~"#{label.name}") }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_issues_url(project.namespace, project, label_name: label.name)
+ expect(doc.text).to eq 'See gfm references'
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Label (#{reference}.)")
+ expect(doc.to_html).to match(%r(\(<a.+><span.+>#{label.name}</span></a>\.\)))
+ end
+
+ it 'ignores invalid label names' do
+ exp = act = %(Label ~"#{label.name.reverse}")
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
new file mode 100644
index 00000000000..d6e745114f2
--- /dev/null
+++ b/spec/lib/gitlab/markdown/merge_request_reference_filter_spec.rb
@@ -0,0 +1,124 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe MergeRequestReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:project) }
+ let(:merge) { create(:merge_request, source_project: project) }
+
+ it 'requires project context' do
+ expect { described_class.call('MergeRequest !123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Merge !#{merge.iid}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ let(:reference) { "!#{merge.iid}" }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_merge_request_url(project.namespace, project, merge)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid merge IDs' do
+ exp = act = "Merge !#{merge.iid + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Merge #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
+ end
+
+ it 'escapes the title attribute' do
+ merge.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = filter("Merge #{reference}")
+ expect(doc.text).to eq "Merge #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Merge #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-merge_request'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Merge #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Merge #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_merge_request_url(project.namespace, project, merge, only_path: true)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Merge #{reference}")
+ expect(result[:references][:merge_request]).to eq [merge]
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:project, namespace: namespace) }
+ let(:merge) { create(:merge_request, source_project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}!#{merge.iid}" }
+
+ context 'when user can access reference' do
+ before { allow_cross_reference! }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_merge_request_url(project2.namespace,
+ project, merge)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Merge (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid merge IDs on the referenced project' do
+ exp = act = "Merge #{project2.path_with_namespace}!#{merge.iid + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Merge #{reference}")
+ expect(result[:references][:merge_request]).to eq [merge]
+ end
+ end
+
+ context 'when user cannot access reference' do
+ before { disallow_cross_reference! }
+
+ it 'ignores valid references' do
+ exp = act = "See #{reference}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/sanitization_filter_spec.rb b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
new file mode 100644
index 00000000000..ab909a68635
--- /dev/null
+++ b/spec/lib/gitlab/markdown/sanitization_filter_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe SanitizationFilter do
+ def filter(html, options = {})
+ described_class.call(html, options)
+ end
+
+ describe 'default whitelist' do
+ it 'sanitizes tags that are not whitelisted' do
+ act = %q{<textarea>no inputs</textarea> and <blink>no blinks</blink>}
+ exp = 'no inputs and no blinks'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'sanitizes tag attributes' do
+ act = %q{<a href="http://example.com/bar.html" onclick="bar">Text</a>}
+ exp = %q{<a href="http://example.com/bar.html">Text</a>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'sanitizes javascript in attributes' do
+ act = %q(<a href="javascript:alert('foo')">Text</a>)
+ exp = '<a>Text</a>'
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'allows whitelisted HTML tags from the user' do
+ exp = act = "<dl>\n<dt>Term</dt>\n<dd>Definition</dd>\n</dl>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ describe 'custom whitelist' do
+ it 'allows `class` attribute on any element' do
+ exp = act = %q{<strong class="foo">Strong</strong>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'allows `id` attribute on any element' do
+ exp = act = %q{<em id="foo">Emphasis</em>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'allows `style` attribute on table elements' do
+ html = <<-HTML.strip_heredoc
+ <table>
+ <tr><th style="text-align: center">Head</th></tr>
+ <tr><td style="text-align: right">Body</th></tr>
+ </table>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('th')['style']).to eq 'text-align: center'
+ expect(doc.at_css('td')['style']).to eq 'text-align: right'
+ end
+
+ it 'allows `span` elements' do
+ exp = act = %q{<span>Hello</span>}
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'removes `rel` attribute from `a` elements' do
+ doc = filter(%q{<a href="#" rel="nofollow">Link</a>})
+
+ expect(doc.css('a').size).to eq 1
+ expect(doc.at_css('a')['href']).to eq '#'
+ expect(doc.at_css('a')['rel']).to be_nil
+ end
+
+ it 'removes script-like `href` attribute from `a` elements' do
+ html = %q{<a href="javascript:alert('Hi')">Hi</a>}
+ doc = filter(html)
+
+ expect(doc.css('a').size).to eq 1
+ expect(doc.at_css('a')['href']).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
new file mode 100644
index 00000000000..a4b331157af
--- /dev/null
+++ b/spec/lib/gitlab/markdown/snippet_reference_filter_spec.rb
@@ -0,0 +1,122 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe SnippetReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:empty_project) }
+ let(:snippet) { create(:project_snippet, project: project) }
+ let(:reference) { "$#{snippet.id}" }
+
+ it 'requires project context' do
+ expect { described_class.call('Snippet $123', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Snippet #{reference}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'internal reference' do
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).to eq urls.
+ namespace_project_snippet_url(project.namespace, project, snippet)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("Snippet (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs' do
+ exp = act = "Snippet $#{snippet.id + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'includes a title attribute' do
+ doc = filter("Snippet #{reference}")
+ expect(doc.css('a').first.attr('title')).to eq "Snippet: #{snippet.title}"
+ end
+
+ it 'escapes the title attribute' do
+ snippet.update_attribute(:title, %{"></a>whatever<a title="})
+
+ doc = filter("Snippet #{reference}")
+ expect(doc.text).to eq "Snippet #{reference}"
+ end
+
+ it 'includes default classes' do
+ doc = filter("Snippet #{reference}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-snippet'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Snippet #{reference}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Snippet #{reference}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.namespace_project_snippet_url(project.namespace, project, snippet, only_path: true)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Snippet #{reference}")
+ expect(result[:references][:snippet]).to eq [snippet]
+ end
+ end
+
+ context 'cross-project reference' do
+ let(:namespace) { create(:namespace, name: 'cross-reference') }
+ let(:project2) { create(:empty_project, namespace: namespace) }
+ let(:snippet) { create(:project_snippet, project: project2) }
+ let(:reference) { "#{project2.path_with_namespace}$#{snippet.id}" }
+
+ context 'when user can access reference' do
+ before { allow_cross_reference! }
+
+ it 'links to a valid reference' do
+ doc = filter("See #{reference}")
+
+ expect(doc.css('a').first.attr('href')).
+ to eq urls.namespace_project_snippet_url(project2.namespace, project2, snippet)
+ end
+
+ it 'links with adjacent text' do
+ doc = filter("See (#{reference}.)")
+ expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(reference)}<\/a>\.\)/)
+ end
+
+ it 'ignores invalid snippet IDs on the referenced project' do
+ exp = act = "See #{project2.path_with_namespace}$#{snippet.id + 1}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Snippet #{reference}")
+ expect(result[:references][:snippet]).to eq [snippet]
+ end
+ end
+
+ context 'when user cannot access reference' do
+ before { disallow_cross_reference! }
+
+ it 'ignores valid references' do
+ exp = act = "See #{reference}"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
new file mode 100644
index 00000000000..f383a5850d5
--- /dev/null
+++ b/spec/lib/gitlab/markdown/table_of_contents_filter_spec.rb
@@ -0,0 +1,101 @@
+# encoding: UTF-8
+
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe TableOfContentsFilter do
+ def filter(html, options = {})
+ described_class.call(html, options)
+ end
+
+ def header(level, text)
+ "<h#{level}>#{text}</h#{level}>\n"
+ end
+
+ it 'does nothing when :no_header_anchors is truthy' do
+ exp = act = header(1, 'Header')
+ expect(filter(act, no_header_anchors: 1).to_html).to eq exp
+ end
+
+ it 'does nothing with empty headers' do
+ exp = act = header(1, nil)
+ expect(filter(act).to_html).to eq exp
+ end
+
+ 1.upto(6) do |i|
+ it "processes h#{i} elements" do
+ html = header(i, "Header #{i}")
+ doc = filter(html)
+
+ expect(doc.css("h#{i} a").first.attr('id')).to eq "header-#{i}"
+ end
+ end
+
+ describe 'anchor tag' do
+ it 'has an `anchor` class' do
+ doc = filter(header(1, 'Header'))
+ expect(doc.css('h1 a').first.attr('class')).to eq 'anchor'
+ end
+
+ it 'links to the id' do
+ doc = filter(header(1, 'Header'))
+ expect(doc.css('h1 a').first.attr('href')).to eq '#header'
+ end
+
+ describe 'generated IDs' do
+ it 'translates spaces to dashes' do
+ doc = filter(header(1, 'This header has spaces in it'))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-has-spaces-in-it'
+ end
+
+ it 'squeezes multiple spaces and dashes' do
+ doc = filter(header(1, 'This---header is poorly-formatted'))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-poorly-formatted'
+ end
+
+ it 'removes punctuation' do
+ doc = filter(header(1, "This, header! is, filled. with @ punctuation?"))
+ expect(doc.css('h1 a').first.attr('id')).to eq 'this-header-is-filled-with-punctuation'
+ end
+
+ it 'appends a unique number to duplicates' do
+ doc = filter(header(1, 'One') + header(2, 'One'))
+
+ expect(doc.css('h1 a').first.attr('id')).to eq 'one'
+ expect(doc.css('h2 a').first.attr('id')).to eq 'one-1'
+ end
+
+ it 'supports Unicode' do
+ doc = filter(header(1, '한글'))
+ expect(doc.css('h1 a').first.attr('id')).to eq '한글'
+ expect(doc.css('h1 a').first.attr('href')).to eq '#한글'
+ end
+ end
+ end
+
+ describe 'result' do
+ def result(html)
+ HTML::Pipeline.new([described_class]).call(html)
+ end
+
+ let(:results) { result(header(1, 'Header 1') + header(2, 'Header 2')) }
+ let(:doc) { Nokogiri::XML::DocumentFragment.parse(results[:toc]) }
+
+ it 'is contained within a `ul` element' do
+ expect(doc.children.first.name).to eq 'ul'
+ expect(doc.children.first.attr('class')).to eq 'section-nav'
+ end
+
+ it 'contains an `li` element for each header' do
+ expect(doc.css('li').length).to eq 2
+
+ links = doc.css('li a')
+
+ expect(links.first.attr('href')).to eq '#header-1'
+ expect(links.first.text).to eq 'Header 1'
+ expect(links.last.attr('href')).to eq '#header-2'
+ expect(links.last.text).to eq 'Header 2'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/markdown/user_reference_filter_spec.rb b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
new file mode 100644
index 00000000000..922502ada33
--- /dev/null
+++ b/spec/lib/gitlab/markdown/user_reference_filter_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe UserReferenceFilter do
+ include ReferenceFilterSpecHelper
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ it 'requires project context' do
+ expect { described_class.call('Example @mention', {}) }.
+ to raise_error(ArgumentError, /:project/)
+ end
+
+ it 'ignores invalid users' do
+ exp = act = 'Hey @somebody'
+ expect(filter(act).to_html).to eq(exp)
+ end
+
+ %w(pre code a style).each do |elem|
+ it "ignores valid references contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>Hey @#{user.username}</#{elem}>"
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+
+ context 'mentioning @all' do
+ before do
+ project.team << [project.creator, :developer]
+ end
+
+ it 'supports a special @all mention' do
+ doc = filter("Hey @all")
+ expect(doc.css('a').length).to eq 1
+ expect(doc.css('a').first.attr('href'))
+ .to eq urls.namespace_project_url(project.namespace, project)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result('Hey @all')
+ expect(result[:references][:user]).to eq [project.creator]
+ end
+ end
+
+ context 'mentioning a user' do
+ let(:reference) { "@#{user.username}" }
+
+ it 'links to a User' do
+ doc = filter("Hey #{reference}")
+ expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
+ end
+
+ # TODO (rspeicher): This test might be overkill
+ it 'links to a User with a period' do
+ user = create(:user, name: 'alphA.Beta')
+
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ # TODO (rspeicher): This test might be overkill
+ it 'links to a User with an underscore' do
+ user = create(:user, name: 'ping_pong_king')
+
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').length).to eq 1
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Hey #{reference}")
+ expect(result[:references][:user]).to eq [user]
+ end
+ end
+
+ context 'mentioning a group' do
+ let(:group) { create(:group) }
+ let(:user) { create(:user) }
+
+ let(:reference) { "@#{group.name}" }
+
+ context 'that the current user can read' do
+ before do
+ group.add_user(user, Gitlab::Access::DEVELOPER)
+ end
+
+ it 'links to the Group' do
+ doc = filter("Hey #{reference}", current_user: user)
+ expect(doc.css('a').first.attr('href')).to eq urls.group_url(group)
+ end
+
+ it 'adds to the results hash' do
+ result = pipeline_result("Hey #{reference}", current_user: user)
+ expect(result[:references][:user]).to eq group.users
+ end
+ end
+
+ context 'that the current user cannot read' do
+ it 'ignores references to the Group' do
+ doc = filter("Hey #{reference}", current_user: user)
+ expect(doc.to_html).to eq "Hey #{reference}"
+ end
+
+ it 'does not add to the results hash' do
+ result = pipeline_result("Hey #{reference}", current_user: user)
+ expect(result[:references][:user]).to eq []
+ end
+ end
+ end
+
+ it 'links with adjacent text' do
+ skip 'TODO (rspeicher): Re-enable when usernames can\'t end in periods.'
+ doc = filter("Mention me (@#{user.username}.)")
+ expect(doc.to_html).to match(/\(<a.+>@#{user.username}<\/a>\.\)/)
+ end
+
+ it 'includes default classes' do
+ doc = filter("Hey @#{user.username}")
+ expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-project_member'
+ end
+
+ it 'includes an optional custom class' do
+ doc = filter("Hey @#{user.username}", reference_class: 'custom')
+ expect(doc.css('a').first.attr('class')).to include 'custom'
+ end
+
+ it 'supports an :only_path context' do
+ doc = filter("Hey @#{user.username}", only_path: true)
+ link = doc.css('a').first.attr('href')
+
+ expect(link).not_to match %r(https?://)
+ expect(link).to eq urls.user_path(user)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
new file mode 100644
index 00000000000..678086ffa14
--- /dev/null
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
@@ -0,0 +1,110 @@
+require 'spec_helper'
+
+describe Gitlab::OAuth::AuthHash do
+ let(:auth_hash) do
+ Gitlab::OAuth::AuthHash.new(
+ double({
+ provider: provider_ascii,
+ uid: uid_ascii,
+ info: double(info_hash)
+ })
+ )
+ end
+
+ let(:uid_raw) {
+ "CN=Onur K\xC3\xBC\xC3\xA7\xC3\xBCk,OU=Test,DC=example,DC=net"
+ }
+ let(:email_raw) { "onur.k\xC3\xBC\xC3\xA7\xC3\xBCk@example.net" }
+ let(:nickname_raw) { "ok\xC3\xBC\xC3\xA7\xC3\xBCk" }
+ let(:first_name_raw) { 'Onur' }
+ let(:last_name_raw) { "K\xC3\xBC\xC3\xA7\xC3\xBCk" }
+ let(:name_raw) { "Onur K\xC3\xBC\xC3\xA7\xC3\xBCk" }
+
+ let(:provider_ascii) { 'ldap'.force_encoding(Encoding::ASCII_8BIT) }
+ let(:uid_ascii) { uid_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:email_ascii) { email_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:nickname_ascii) { nickname_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:first_name_ascii) { first_name_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:last_name_ascii) { last_name_raw.force_encoding(Encoding::ASCII_8BIT) }
+ let(:name_ascii) { name_raw.force_encoding(Encoding::ASCII_8BIT) }
+
+ let(:provider_utf8) { provider_ascii.force_encoding(Encoding::UTF_8) }
+ let(:uid_utf8) { uid_ascii.force_encoding(Encoding::UTF_8) }
+ let(:email_utf8) { email_ascii.force_encoding(Encoding::UTF_8) }
+ let(:nickname_utf8) { nickname_ascii.force_encoding(Encoding::UTF_8) }
+ let(:name_utf8) { name_ascii.force_encoding(Encoding::UTF_8) }
+
+ let(:info_hash) {
+ {
+ email: email_ascii,
+ first_name: first_name_ascii,
+ last_name: last_name_ascii,
+ name: name_ascii,
+ nickname: nickname_ascii,
+ uid: uid_ascii
+ }
+ }
+
+ context 'defaults' do
+ it { expect(auth_hash.provider).to eql provider_utf8 }
+ it { expect(auth_hash.uid).to eql uid_utf8 }
+ it { expect(auth_hash.email).to eql email_utf8 }
+ it { expect(auth_hash.username).to eql nickname_utf8 }
+ it { expect(auth_hash.name).to eql name_utf8 }
+ it { expect(auth_hash.password).to_not be_empty }
+ end
+
+ context 'email not provided' do
+ before { info_hash.delete(:email) }
+
+ it 'generates a temp email' do
+ expect( auth_hash.email).to start_with('temp-email-for-oauth')
+ end
+ end
+
+ context 'username not provided' do
+ before { info_hash.delete(:nickname) }
+
+ it 'takes the first part of the email as username' do
+ expect(auth_hash.username).to eql 'onur-kucuk'
+ end
+ end
+
+ context 'name not provided' do
+ before { info_hash.delete(:name) }
+
+ it 'concats first and lastname as the name' do
+ expect(auth_hash.name).to eql name_utf8
+ end
+ end
+
+ context 'auth_hash constructed with ASCII-8BIT encoding' do
+ it 'forces utf8 encoding on uid' do
+ auth_hash.uid.encoding.should eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on provider' do
+ auth_hash.provider.encoding.should eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on name' do
+ auth_hash.name.encoding.should eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on full_name' do
+ auth_hash.full_name.encoding.should eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on username' do
+ auth_hash.username.encoding.should eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on email' do
+ auth_hash.email.encoding.should eql Encoding::UTF_8
+ end
+
+ it 'forces utf8 encoding on password' do
+ auth_hash.password.encoding.should eql Encoding::UTF_8
+ end
+ end
+end
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/oauth/auth_hash_spec.rb b/spec/lib/gitlab/oauth/auth_hash_spec.rb
deleted file mode 100644
index 5eb77b492b2..00000000000
--- a/spec/lib/gitlab/oauth/auth_hash_spec.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::OAuth::AuthHash do
- let(:auth_hash) do
- Gitlab::OAuth::AuthHash.new(double({
- provider: 'twitter',
- uid: uid,
- info: double(info_hash)
- }))
- end
- let(:uid) { 'my-uid' }
- let(:email) { 'my-email@example.com' }
- let(:nickname) { 'my-nickname' }
- let(:info_hash) {
- {
- email: email,
- nickname: nickname,
- name: 'John',
- first_name: "John",
- last_name: "Who"
- }
- }
-
- context "defaults" do
- it { expect(auth_hash.provider).to eql 'twitter' }
- it { expect(auth_hash.uid).to eql uid }
- it { expect(auth_hash.email).to eql email }
- it { expect(auth_hash.username).to eql nickname }
- it { expect(auth_hash.name).to eql "John" }
- it { expect(auth_hash.password).to_not be_empty }
- end
-
- context "email not provided" do
- before { info_hash.delete(:email) }
- it "generates a temp email" do
- expect( auth_hash.email).to start_with('temp-email-for-oauth')
- end
- end
-
- context "username not provided" do
- before { info_hash.delete(:nickname) }
-
- it "takes the first part of the email as username" do
- expect( auth_hash.username ).to eql "my-email"
- end
- end
-
- context "name not provided" do
- before { info_hash.delete(:name) }
-
- it "concats first and lastname as the name" do
- expect( auth_hash.name ).to eql "John Who"
- end
- end
-end \ No newline at end of file
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 034f8ee7c45..9801dc16554 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -1,123 +1,91 @@
require 'spec_helper'
describe Gitlab::ReferenceExtractor do
- it 'extracts username references' do
- subject.analyze('this contains a @user reference', nil)
- expect(subject.users).to eq([{ project: nil, id: 'user' }])
- end
+ let(:project) { create(:project) }
+ subject { Gitlab::ReferenceExtractor.new(project, project.creator) }
- it 'extracts issue references' do
- subject.analyze('this one talks about issue #1234', nil)
- expect(subject.issues).to eq([{ project: nil, id: '1234' }])
- end
+ it 'accesses valid user objects' do
+ @u_foo = create(:user, username: 'foo')
+ @u_bar = create(:user, username: 'bar')
+ @u_offteam = create(:user, username: 'offteam')
- 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' }])
- end
+ project.team << [@u_foo, :reporter]
+ project.team << [@u_bar, :guest]
- 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('@foo, @baduser, @bar, and @offteam')
+ expect(subject.users).to eq([@u_foo, @u_bar, @u_offteam])
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' }])
- end
+ it 'accesses valid issue objects' do
+ @i0 = create(:issue, project: project)
+ @i1 = create(:issue, project: project)
- 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("##{@i0.iid}, ##{@i1.iid}, and #999.")
+ expect(subject.issues).to eq([@i0, @i1])
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' }])
- 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 '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("!999, !#{@m1.iid}, and !#{@m0.iid}.")
+ expect(subject.merge_requests).to eq([@m1, @m0])
end
- it 'leaves the original note unmodified' do
- text = 'issue #123 is just the worst, @user'
- subject.analyze(text, nil)
- expect(text).to eq('issue #123 is just the worst, @user')
- end
+ it 'accesses valid labels' do
+ @l0 = create(:label, title: 'one', project: project)
+ @l1 = create(:label, title: 'two', project: project)
+ @l2 = create(:label)
- it 'handles all possible kinds of references' do
- accessors = Gitlab::Markdown::TYPES.map { |t| "#{t}s".to_sym }
- expect(subject).to respond_to(*accessors)
+ subject.analyze("~#{@l0.id}, ~999, ~#{@l2.id}, ~#{@l1.id}")
+ expect(subject.labels).to eq([@l0, @l1])
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')
-
- 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
+ it 'accesses valid snippets' do
+ @s0 = create(:project_snippet, project: project)
+ @s1 = create(:project_snippet, project: project)
+ @s2 = create(:project_snippet)
- it 'accesses valid issue objects' do
- @i0 = create(:issue, project: project)
- @i1 = create(:issue, project: project)
+ subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}")
+ expect(subject.snippets).to eq([@s0, @s1])
+ end
- subject.analyze("##{@i0.iid}, ##{@i1.iid}, and #999.", project)
- expect(subject.issues_for(project)).to eq([@i0, @i1])
- end
+ it 'accesses valid commits' do
+ commit = project.commit('master')
- 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("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
- subject.analyze("!999, !#{@m1.iid}, and !#{@m0.iid}.", project)
- expect(subject.merge_requests_for(project)).to eq([@m1, @m0])
- end
+ it 'accesses valid commit ranges' do
+ commit = project.commit('master')
+ earlier_commit = project.commit('master~2')
- it 'accesses valid snippets' do
- @s0 = create(:project_snippet, project: project)
- @s1 = create(:project_snippet, project: project)
- @s2 = create(:project_snippet)
+ subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}")
- subject.analyze("$#{@s0.id}, $999, $#{@s2.id}, $#{@s1.id}", project)
- expect(subject.snippets_for(project)).to eq([@s0, @s1])
- end
+ extracted = subject.commit_ranges
+ expect(extracted.size).to eq(1)
+ expect(extracted.first).to be_kind_of(CommitRange)
+ expect(extracted.first.commit_from).to eq earlier_commit
+ expect(extracted.first.commit_to).to eq commit
+ end
- it 'accesses valid commits' do
- commit = project.repository.commit('master')
+ context 'with a project with an underscore' do
+ let(:other_project) { create(:project, path: 'test_project') }
+ let(:issue) { create(:issue, project: other_project) }
- 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)
+ before do
+ other_project.team << [project.creator, :developer]
end
- 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)
+ it 'handles project issue references' do
+ subject.analyze("this refers issue #{other_project.path_with_namespace}##{issue.iid}")
+ extracted = subject.issues
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)
+ 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..7fdc8fa600d 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -1,14 +1,15 @@
+# coding: utf-8
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
@@ -16,6 +17,8 @@ describe Gitlab::Regex do
it { expect('GitLab CE').to match(Gitlab::Regex.project_name_regex) }
it { expect('100 lines').to match(Gitlab::Regex.project_name_regex) }
it { expect('gitlab.git').to match(Gitlab::Regex.project_name_regex) }
+ it { expect('Český název').to match(Gitlab::Regex.project_name_regex) }
+ it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) }
it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) }
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 4090fa46205..dbcf7286e45 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -5,11 +5,14 @@ describe Notify do
include EmailSpec::Matchers
include RepoHelpers
+ 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) }
before(:each) do
+ ActionMailer::Base.deliveries.clear
email = recipient.emails.create(email: "notifications@example.com")
recipient.update_attribute(:notification_email, email.email)
end
@@ -23,9 +26,14 @@ describe Notify do
shared_examples 'an email sent from GitLab' do
it 'is sent from GitLab' do
sender = subject.header[:from].addrs[0]
- expect(sender.display_name).to eq('GitLab')
+ 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|
@@ -457,7 +465,7 @@ describe Notify do
end
describe 'on a commit' do
- let(:commit) { project.repository.commit }
+ let(:commit) { project.commit }
before(:each) { allow(note).to receive(:noteable).and_return(commit) }
@@ -563,15 +571,109 @@ describe Notify do
end
end
+ describe 'email on push for a created branch' do
+ let(:example_site_path) { root_path }
+ let(:user) { create(:user) }
+ let(:tree_path) { namespace_project_tree_path(project.namespace, project, "master") }
+
+ subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) }
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'is sent to recipient' do
+ is_expected.to deliver_to 'devs@company.name'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /Pushed new branch master/
+ end
+
+ it 'contains a link to the branch' do
+ is_expected.to have_body_text /#{tree_path}/
+ end
+ end
+
+ describe 'email on push for a created tag' do
+ let(:example_site_path) { root_path }
+ let(:user) { create(:user) }
+ let(:tree_path) { namespace_project_tree_path(project.namespace, project, "v1.0") }
+
+ subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) }
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'is sent to recipient' do
+ is_expected.to deliver_to 'devs@company.name'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /Pushed new tag v1\.0/
+ end
+
+ it 'contains a link to the tag' do
+ is_expected.to have_body_text /#{tree_path}/
+ end
+ end
+
+ describe 'email on push for a deleted branch' do
+ let(:example_site_path) { root_path }
+ let(:user) { create(:user) }
+
+ subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) }
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'is sent to recipient' do
+ is_expected.to deliver_to 'devs@company.name'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /Deleted branch master/
+ end
+ end
+
+ describe 'email on push for a deleted tag' do
+ let(:example_site_path) { root_path }
+ let(:user) { create(:user) }
+
+ subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) }
+
+ it 'is sent as the author' do
+ sender = subject.header[:from].addrs[0]
+ expect(sender.display_name).to eq(user.name)
+ expect(sender.address).to eq(gitlab_sender)
+ end
+
+ it 'is sent to recipient' do
+ is_expected.to deliver_to 'devs@company.name'
+ end
+
+ it 'has the correct subject' do
+ is_expected.to have_subject /Deleted tag v1\.0/
+ end
+ end
+
describe 'email on push with multiple commits' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_image_commit.id, sample_commit.id) }
- let(:commits) { Commit.decorate(compare.commits) }
- let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base), to: Commit.new(compare.head)) }
+ let(:commits) { Commit.decorate(compare.commits, nil) }
+ let(:diff_path) { namespace_project_compare_path(project.namespace, project, from: Commit.new(compare.base, project), to: Commit.new(compare.head, project)) }
let(:send_from_committer_email) { false }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare, false, send_from_committer_email) }
+ subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) }
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
@@ -622,6 +724,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
@@ -635,6 +742,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
@@ -648,6 +760,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
@@ -656,10 +773,10 @@ describe Notify do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
let(:compare) { Gitlab::Git::Compare.new(project.repository.raw_repository, sample_commit.parent_id, sample_commit.id) }
- let(:commits) { Commit.decorate(compare.commits) }
+ let(:commits) { Commit.decorate(compare.commits, nil) }
let(:diff_path) { namespace_project_commit_path(project.namespace, project, commits.first) }
- subject { Notify.repository_push_email(project.id, 'devs@company.name', user.id, 'master', compare) }
+ subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) }
it 'is sent as the author' do
sender = subject.header[:from].addrs[0]
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index d1027f64d13..116c318121d 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -2,21 +2,50 @@
#
# Table name: application_settings
#
-# id :integer not null, primary key
-# default_projects_limit :integer
-# signup_enabled :boolean
-# signin_enabled :boolean
-# gravatar_enabled :boolean
-# sign_in_text :text
-# created_at :datetime
-# updated_at :datetime
-# home_page_url :string(255)
-# default_branch_protection :integer default(2)
-# twitter_sharing_enabled :boolean default(TRUE)
+# id :integer not null, primary key
+# default_projects_limit :integer
+# signup_enabled :boolean
+# signin_enabled :boolean
+# gravatar_enabled :boolean
+# sign_in_text :text
+# created_at :datetime
+# updated_at :datetime
+# home_page_url :string(255)
+# default_branch_protection :integer default(2)
+# twitter_sharing_enabled :boolean default(TRUE)
+# restricted_visibility_levels :text
+# max_attachment_size :integer default(10), not null
+# default_project_visibility :integer
+# default_snippet_visibility :integer
+# restricted_signup_domains :text
#
require 'spec_helper'
describe ApplicationSetting, models: true do
it { expect(ApplicationSetting.create_from_defaults).to be_valid }
+
+ context 'restricted signup domains' do
+ let(:setting) { ApplicationSetting.create_from_defaults }
+
+ it 'set single domain' do
+ setting.restricted_signup_domains_raw = 'example.com'
+ expect(setting.restricted_signup_domains).to eq(['example.com'])
+ end
+
+ it 'set multiple domains with spaces' do
+ setting.restricted_signup_domains_raw = 'example.com *.example.com'
+ expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ end
+
+ it 'set multiple domains with newlines and a space' do
+ setting.restricted_signup_domains_raw = "example.com\n *.example.com"
+ expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ end
+
+ it 'set multiple domains with commas' do
+ setting.restricted_signup_domains_raw = "example.com, *.example.com"
+ expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com'])
+ end
+ end
end
diff --git a/spec/models/commit_range_spec.rb b/spec/models/commit_range_spec.rb
new file mode 100644
index 00000000000..31ee3e99cad
--- /dev/null
+++ b/spec/models/commit_range_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe CommitRange do
+ let(:sha_from) { 'f3f85602' }
+ let(:sha_to) { 'e86e1013' }
+
+ let(:range) { described_class.new("#{sha_from}...#{sha_to}") }
+ let(:range2) { described_class.new("#{sha_from}..#{sha_to}") }
+
+ it 'raises ArgumentError when given an invalid range string' do
+ expect { described_class.new("Foo") }.to raise_error
+ end
+
+ describe '#to_s' do
+ it 'is correct for three-dot syntax' do
+ expect(range.to_s).to eq "#{sha_from[0..7]}...#{sha_to[0..7]}"
+ end
+
+ it 'is correct for two-dot syntax' do
+ expect(range2.to_s).to eq "#{sha_from[0..7]}..#{sha_to[0..7]}"
+ end
+ end
+
+ describe '#reference_title' do
+ it 'returns the correct String for three-dot ranges' do
+ expect(range.reference_title).to eq "Commits #{sha_from} through #{sha_to}"
+ end
+
+ it 'returns the correct String for two-dot ranges' do
+ expect(range2.reference_title).to eq "Commits #{sha_from}^ through #{sha_to}"
+ end
+ end
+
+ describe '#to_param' do
+ it 'includes the correct keys' do
+ expect(range.to_param.keys).to eq %i(from to)
+ end
+
+ it 'includes the correct values for a three-dot range' do
+ expect(range.to_param).to eq({from: sha_from, to: sha_to})
+ end
+
+ it 'includes the correct values for a two-dot range' do
+ expect(range2.to_param).to eq({from: sha_from + '^', to: sha_to})
+ end
+ end
+
+ describe '#exclude_start?' do
+ it 'is false for three-dot ranges' do
+ expect(range.exclude_start?).to eq false
+ end
+
+ it 'is true for two-dot ranges' do
+ expect(range2.exclude_start?).to eq true
+ end
+ end
+
+ describe '#valid_commits?' do
+ context 'without a project' do
+ it 'returns nil' do
+ expect(range.valid_commits?).to be_nil
+ end
+ end
+
+ it 'accepts an optional project argument' do
+ project1 = double('project1').as_null_object
+ project2 = double('project2').as_null_object
+
+ # project1 gets assigned through the accessor, but ignored when not given
+ # as an argument to `valid_commits?`
+ expect(project1).not_to receive(:present?)
+ range.project = project1
+
+ # project2 gets passed to `valid_commits?`
+ expect(project2).to receive(:present?).and_return(false)
+
+ range.valid_commits?(project2)
+ end
+
+ context 'with a project' do
+ let(:project) { double('project', repository: double('repository')) }
+
+ context 'with a valid repo' do
+ before do
+ expect(project).to receive(:valid_repo?).and_return(true)
+ range.project = project
+ end
+
+ it 'is false when `sha_from` is invalid' do
+ expect(project.repository).to receive(:commit).with(sha_from).and_return(false)
+ expect(project.repository).not_to receive(:commit).with(sha_to)
+ expect(range).not_to be_valid_commits
+ end
+
+ it 'is false when `sha_to` is invalid' do
+ expect(project.repository).to receive(:commit).with(sha_from).and_return(true)
+ expect(project.repository).to receive(:commit).with(sha_to).and_return(false)
+ expect(range).not_to be_valid_commits
+ end
+
+ it 'is true when both `sha_from` and `sha_to` are valid' do
+ expect(project.repository).to receive(:commit).with(sha_from).and_return(true)
+ expect(project.repository).to receive(:commit).with(sha_to).and_return(true)
+ expect(range).to be_valid_commits
+ end
+ end
+
+ context 'without a valid repo' do
+ before do
+ expect(project).to receive(:valid_repo?).and_return(false)
+ range.project = project
+ end
+
+ it 'returns false' do
+ expect(range).not_to be_valid_commits
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 8b3d88640da..ad2ac143d97 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Commit do
let(:project) { create :project }
- let(:commit) { project.repository.commit }
+ let(:commit) { project.commit }
describe '#title' do
it "returns no_commit_message when safe_message is blank" do
@@ -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
@@ -58,19 +58,20 @@ eos
it 'detects issues that this commit is marked as closing' do
commit.stub(safe_message: "Fixes ##{issue.iid}")
- expect(commit.closes_issues(project)).to eq([issue])
+ expect(commit.closes_issues).to eq([issue])
end
it 'does not detect issues from other projects' do
ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}"
commit.stub(safe_message: "Fixes #{ext_ref}")
- expect(commit.closes_issues(project)).to be_empty
+ expect(commit.closes_issues).to be_empty
end
end
it_behaves_like 'a mentionable' do
- let(:subject) { commit }
- let(:mauthor) { create :user, email: commit.author_email }
+ subject { commit }
+
+ let(:author) { create(:user, email: commit.author_email) }
let(:backref_text) { "commit #{subject.id}" }
let(:set_mentionable_text) { ->(txt){ subject.stub(safe_message: txt) } }
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index b32be8d7a7c..95729932459 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -10,6 +10,7 @@
# title :string(255)
# type :string(255)
# fingerprint :string(255)
+# public :boolean default(FALSE), not null
#
require 'spec_helper'
diff --git a/spec/models/deploy_keys_project_spec.rb b/spec/models/deploy_keys_project_spec.rb
index aacd9bf38bf..7032b777144 100644
--- a/spec/models/deploy_keys_project_spec.rb
+++ b/spec/models/deploy_keys_project_spec.rb
@@ -21,4 +21,52 @@ describe DeployKeysProject do
it { is_expected.to validate_presence_of(:project_id) }
it { is_expected.to validate_presence_of(:deploy_key_id) }
end
+
+ describe "Destroying" do
+ let(:project) { create(:project) }
+ subject { create(:deploy_keys_project, project: project) }
+ let(:deploy_key) { subject.deploy_key }
+
+ context "when the deploy key is only used by this project" do
+ context "when the deploy key is public" do
+ before do
+ deploy_key.update_attribute(:public, true)
+ end
+
+ 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
+ other_project.deploy_keys << deploy_key
+ end
+
+ it "doesn't destroy the deploy key" do
+ subject.destroy
+
+ expect {
+ deploy_key.reload
+ }.not_to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
end
diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb
new file mode 100644
index 00000000000..f2e77fc88c4
--- /dev/null
+++ b/spec/models/external_wiki_service_spec.rb
@@ -0,0 +1,59 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+require 'spec_helper'
+
+describe ExternalWikiService do
+ include ExternalWikiHelper
+ describe "Associations" do
+ it { should belong_to :project }
+ it { should have_one :service_hook }
+ end
+
+ describe "Validations" do
+ context "active" do
+ before do
+ subject.active = true
+ end
+
+ it { should validate_presence_of :external_wiki_url }
+ end
+ end
+
+ describe 'External wiki' do
+ let(:project) { create(:project) }
+
+ context 'when it is active' do
+ before do
+ properties = { 'external_wiki_url' => 'https://gitlab.com' }
+ @service = project.create_external_wiki_service(active: true, properties: properties)
+ end
+
+ after do
+ @service.destroy!
+ end
+
+ it 'should replace the wiki url' do
+ wiki_path = get_project_wiki_path(project)
+ wiki_path.should match('https://gitlab.com')
+ end
+ end
+ end
+end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 087e40c3d84..20d823b40e5 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -56,7 +56,8 @@ describe Issue do
end
it_behaves_like 'an editable mentionable' do
- let(:subject) { create :issue, project: mproject }
+ subject { create(:issue, project: project) }
+
let(:backref_text) { "issue ##{subject.iid}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index a212b95a7d6..6eb1208a7f2 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -10,6 +10,7 @@
# title :string(255)
# type :string(255)
# fingerprint :string(255)
+# public :boolean default(FALSE), not null
#
require 'spec_helper'
@@ -58,12 +59,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..57f840c1e91
--- /dev/null
+++ b/spec/models/member_spec.rb
@@ -0,0 +1,167 @@
+# == Schema Information
+#
+# Table name: members
+#
+# id :integer not null, primary key
+# access_level :integer not null
+# source_id :integer not null
+# source_type :string(255) not null
+# user_id :integer
+# notification_level :integer not null
+# type :string(255)
+# created_at :datetime
+# updated_at :datetime
+# created_by_id :integer
+# invite_email :string(255)
+# invite_token :string(255)
+# invite_accepted_at :datetime
+#
+
+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/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index e04f1741b24..7c10c9f0f48 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -6,11 +6,15 @@
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
-# user_id :integer not null
+# user_id :integer
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string(255)
+# invite_token :string(255)
+# invite_accepted_at :datetime
#
require 'spec_helper'
@@ -28,18 +32,18 @@ describe GroupMember do
describe "#after_update" do
before do
- @membership = create :group_member
- @membership.stub(notification_service: double('NotificationService').as_null_object)
+ @group_member = create :group_member
+ @group_member.stub(notification_service: double('NotificationService').as_null_object)
end
it "should send email to user" do
- expect(@membership).to receive(:notification_service)
- @membership.update_attribute(:access_level, GroupMember::MASTER)
+ expect(@group_member).to receive(:notification_service)
+ @group_member.update_attribute(:access_level, GroupMember::MASTER)
end
it "does not send an email when the access level has not changed" do
- expect(@membership).not_to receive(:notification_service)
- @membership.update_attribute(:access_level, GroupMember::OWNER)
+ expect(@group_member).not_to receive(:notification_service)
+ @group_member.update_attribute(:access_level, GroupMember::OWNER)
end
end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 521721f3577..5c72cfe1d6a 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -6,11 +6,15 @@
# access_level :integer not null
# source_id :integer not null
# source_type :string(255) not null
-# user_id :integer not null
+# user_id :integer
# notification_level :integer not null
# type :string(255)
# created_at :datetime
# updated_at :datetime
+# created_by_id :integer
+# invite_email :string(255)
+# invite_token :string(255)
+# invite_accepted_at :datetime
#
require 'spec_helper'
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/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index d40503d791c..97b8abc49dd 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -115,13 +115,40 @@ describe MergeRequest do
end
end
+ describe "#work_in_progress?" do
+ it "detects the 'WIP ' prefix" do
+ subject.title = "WIP #{subject.title}"
+ expect(subject).to be_work_in_progress
+ end
+
+ it "detects the 'WIP: ' prefix" do
+ subject.title = "WIP: #{subject.title}"
+ expect(subject).to be_work_in_progress
+ end
+
+ it "detects the '[WIP] ' prefix" do
+ subject.title = "[WIP] #{subject.title}"
+ expect(subject).to be_work_in_progress
+ end
+
+ it "doesn't detect WIP for words starting with WIP" do
+ subject.title = "Wipwap #{subject.title}"
+ expect(subject).not_to be_work_in_progress
+ end
+
+ it "doesn't detect WIP by default" do
+ expect(subject).not_to be_work_in_progress
+ end
+ end
+
it_behaves_like 'an editable mentionable' do
- let(:subject) { create :merge_request, source_project: mproject, target_project: mproject }
+ subject { create(:merge_request, source_project: project, target_project: project) }
+
let(:backref_text) { "merge request !#{subject.iid}" }
let(:set_mentionable_text) { ->(txt){ subject.title = txt } }
end
it_behaves_like 'a Taskable' do
- let(:subject) { create :merge_request, :simple }
+ subject { create :merge_request, :simple }
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/note_spec.rb b/spec/models/note_spec.rb
index 17cb439c90e..4a6bfdb2910 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -182,14 +182,14 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to match(/Status changed to #{status}/) }
+ it { is_expected.to eq("Status changed to #{status}") }
end
it 'appends a back-reference if a closing mentionable is supplied' do
commit = double('commit', gfm_reference: 'commit 123456')
n = Note.create_status_change_note(thing, project, author, status, commit)
- expect(n.note).to match(/Status changed to #{status} by commit 123456/)
+ expect(n.note).to eq("Status changed to #{status} by commit 123456")
end
end
@@ -197,7 +197,7 @@ describe Note do
let(:project) { create(:project) }
let(:thing) { create(:issue, project: project) }
let(:author) { create(:user) }
- let(:assignee) { create(:user) }
+ let(:assignee) { create(:user, username: "assigned_user") }
subject { Note.create_assignee_change_note(thing, project, author, assignee) }
@@ -227,7 +227,7 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to match(/Reassigned to @#{assignee.username}/) }
+ it { is_expected.to eq('Reassigned to @assigned_user') }
end
context 'assignee is removed' do
@@ -235,9 +235,93 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to match(/Assignee removed/) }
+ it { is_expected.to eq('Assignee removed') }
+ end
+ end
+ end
+
+ describe '#create_labels_change_note' do
+ let(:project) { create(:project) }
+ let(:thing) { create(:issue, project: project) }
+ let(:author) { create(:user) }
+ let(:label1) { create(:label) }
+ let(:label2) { create(:label) }
+ let(:added_labels) { [label1, label2] }
+ let(:removed_labels) { [] }
+
+ subject { Note.create_labels_change_note(thing, project, author, added_labels, removed_labels) }
+
+ context 'creates and saves a Note' do
+ it { is_expected.to be_a Note }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.not_to be_nil }
+ end
+ end
+
+ describe '#noteable' do
+ subject { super().noteable }
+ it { is_expected.to eq(thing) }
+ end
+
+ describe '#project' do
+ subject { super().project }
+ it { is_expected.to eq(thing.project) }
+ end
+
+ describe '#author' do
+ subject { super().author }
+ it { is_expected.to eq(author) }
+ end
+
+ describe '#note' do
+ subject { super().note }
+ it { is_expected.to eq("Added ~#{label1.id} ~#{label2.id} labels") }
+ end
+
+ context 'label is removed' do
+ let(:added_labels) { [label1] }
+ let(:removed_labels) { [label2] }
+
+ describe '#note' do
+ subject { super().note }
+ it { is_expected.to eq("Added ~#{label1.id} and removed ~#{label2.id} labels") }
+ end
+ end
+ end
+
+ describe '#create_milestone_change_note' do
+ let(:project) { create(:project) }
+ let(:thing) { create(:issue, project: project) }
+ let(:milestone) { create(:milestone, project: project, title: "first_milestone") }
+ let(:author) { create(:user) }
+
+ subject { Note.create_milestone_change_note(thing, project, author, milestone) }
+
+ context 'creates and saves a Note' do
+ it { is_expected.to be_a Note }
+
+ describe '#id' do
+ subject { super().id }
+ it { is_expected.not_to be_nil }
end
end
+
+ describe '#project' do
+ subject { super().project }
+ it { is_expected.to eq(thing.project) }
+ end
+
+ describe '#author' do
+ subject { super().author }
+ it { is_expected.to eq(author) }
+ end
+
+ describe '#note' do
+ subject { super().note }
+ it { is_expected.to eq("Milestone changed to first_milestone") }
+ end
end
describe '#create_cross_reference_note' do
@@ -245,13 +329,13 @@ describe Note do
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
- let(:commit) { project.repository.commit }
+ let(:commit) { project.commit }
# Test all of {issue, merge request, commit} in both the referenced and referencing
# roles, to ensure that the correct information can be inferred from any argument.
context 'issue from a merge request' do
- subject { Note.create_cross_reference_note(issue, mergereq, author, project) }
+ subject { Note.create_cross_reference_note(issue, mergereq, author) }
it { is_expected.to be_valid }
@@ -272,12 +356,12 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to eq("_mentioned in merge request !#{mergereq.iid}_") }
+ it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") }
end
end
context 'issue from a commit' do
- subject { Note.create_cross_reference_note(issue, commit, author, project) }
+ subject { Note.create_cross_reference_note(issue, commit, author) }
it { is_expected.to be_valid }
@@ -288,12 +372,12 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to eq("_mentioned in commit #{commit.sha}_") }
+ it { is_expected.to eq("mentioned in commit #{commit.sha}") }
end
end
context 'merge request from an issue' do
- subject { Note.create_cross_reference_note(mergereq, issue, author, project) }
+ subject { Note.create_cross_reference_note(mergereq, issue, author) }
it { is_expected.to be_valid }
@@ -309,12 +393,12 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to eq("_mentioned in issue ##{issue.iid}_") }
+ it { is_expected.to eq("mentioned in issue ##{issue.iid}") }
end
end
context 'commit from a merge request' do
- subject { Note.create_cross_reference_note(commit, mergereq, author, project) }
+ subject { Note.create_cross_reference_note(commit, mergereq, author) }
it { is_expected.to be_valid }
@@ -330,18 +414,18 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to eq("_mentioned in merge request !#{mergereq.iid}_") }
+ it { is_expected.to eq("mentioned in merge request !#{mergereq.iid}") }
end
end
context 'commit contained in a merge request' do
- subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author, project) }
+ subject { Note.create_cross_reference_note(mergereq.commits.first, mergereq, author) }
it { is_expected.to be_nil }
end
context 'commit from issue' do
- subject { Note.create_cross_reference_note(commit, issue, author, project) }
+ subject { Note.create_cross_reference_note(commit, issue, author) }
it { is_expected.to be_valid }
@@ -362,13 +446,13 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to eq("_mentioned in issue ##{issue.iid}_") }
+ it { is_expected.to eq("mentioned in issue ##{issue.iid}") }
end
end
context 'commit from commit' do
let(:parent_commit) { commit.parents.first }
- subject { Note.create_cross_reference_note(commit, parent_commit, author, project) }
+ subject { Note.create_cross_reference_note(commit, parent_commit, author) }
it { is_expected.to be_valid }
@@ -389,7 +473,7 @@ describe Note do
describe '#note' do
subject { super().note }
- it { is_expected.to eq("_mentioned in commit #{parent_commit.id}_") }
+ it { is_expected.to eq("mentioned in commit #{parent_commit.id}") }
end
end
end
@@ -398,11 +482,11 @@ describe Note do
let(:project) { create :project }
let(:author) { create :user }
let(:issue) { create :issue }
- let(:commit0) { project.repository.commit }
- let(:commit1) { project.repository.commit('HEAD~2') }
+ let(:commit0) { project.commit }
+ let(:commit1) { project.commit('HEAD~2') }
before do
- Note.create_cross_reference_note(issue, commit0, author, project)
+ Note.create_cross_reference_note(issue, commit0, author)
end
it 'detects if a mentionable has already been mentioned' do
@@ -415,12 +499,47 @@ describe Note do
context 'commit on commit' do
before do
- Note.create_cross_reference_note(commit0, commit1, author, project)
+ Note.create_cross_reference_note(commit0, commit1, author)
end
it { expect(Note.cross_reference_exists?(commit0, commit1)).to be_truthy }
it { expect(Note.cross_reference_exists?(commit1, commit0)).to be_falsey }
end
+
+ context 'legacy note with Markdown emphasis' do
+ let(:issue2) { create :issue, project: project }
+ let!(:note) do
+ create :note, system: true, noteable_id: issue2.id,
+ noteable_type: "Issue", note: "_mentioned in issue " \
+ "#{issue.project.path_with_namespace}##{issue.iid}_"
+ end
+
+ it 'detects if a mentionable with emphasis has been mentioned' do
+ expect(Note.cross_reference_exists?(issue2, issue)).to be_truthy
+ end
+ end
+ end
+
+ describe '#cross_references_with_underscores?' do
+ let(:project) { create :project, path: "first_project" }
+ let(:second_project) { create :project, path: "second_project" }
+
+ let(:author) { create :user }
+ let(:issue0) { create :issue, project: project }
+ let(:issue1) { create :issue, project: second_project }
+ let!(:note) { Note.create_cross_reference_note(issue0, issue1, author) }
+
+ it 'detects if a mentionable has already been mentioned' do
+ expect(Note.cross_reference_exists?(issue0, issue1)).to be_truthy
+ end
+
+ it 'detects if a mentionable has not already been mentioned' do
+ expect(Note.cross_reference_exists?(issue1, issue0)).to be_falsey
+ end
+
+ it 'detects that text has underscores' do
+ expect(note.note).to eq("mentioned in issue #{second_project.path_with_namespace}##{issue1.iid}")
+ end
end
describe '#system?' do
@@ -429,6 +548,8 @@ describe Note do
let(:other) { create(:issue, project: project) }
let(:author) { create(:user) }
let(:assignee) { create(:user) }
+ let(:label) { create(:label) }
+ let(:milestone) { create(:milestone) }
it 'should recognize user-supplied notes as non-system' do
@note = create(:note_on_issue)
@@ -441,7 +562,7 @@ describe Note do
end
it 'should identify cross-reference notes as system notes' do
- @note = Note.create_cross_reference_note(issue, other, author, project)
+ @note = Note.create_cross_reference_note(issue, other, author)
expect(@note).to be_system
end
@@ -449,6 +570,16 @@ describe Note do
@note = Note.create_assignee_change_note(issue, project, author, assignee)
expect(@note).to be_system
end
+
+ it 'should identify label-change notes as system notes' do
+ @note = Note.create_labels_change_note(issue, project, author, [label], [])
+ expect(@note).to be_system
+ end
+
+ it 'should identify milestone-change notes as system notes' do
+ @note = Note.create_milestone_change_note(issue, project, author, milestone)
+ expect(@note).to be_system
+ end
end
describe :authorization do
@@ -498,8 +629,9 @@ describe Note do
end
it_behaves_like 'an editable mentionable' do
+ subject { create :note, noteable: issue, project: project }
+
let(:issue) { create :issue, project: project }
- let(:subject) { create :note, noteable: issue, project: project }
let(:backref_text) { issue.gfm_reference }
let(:set_mentionable_text) { ->(txt) { subject.note = txt } }
end
diff --git a/spec/models/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 13c8d54a2af..cc1f99e0c72 100644
--- a/spec/models/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/assembla_service_spec.rb b/spec/models/project_services/assembla_service_spec.rb
index 91730da1eec..9aee754dd63 100644
--- a/spec/models/project_services/assembla_service_spec.rb
+++ b/spec/models/project_services/assembla_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/buildbox_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 39d7df54cf0..6db54243eac 100644
--- a/spec/models/project_services/buildbox_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -15,11 +15,12 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
-describe BuildboxService do
+describe BuildkiteService do
describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -32,11 +33,11 @@ describe BuildboxService do
default_branch: 'default-brancho'
)
- @service = BuildboxService.new
+ @service = BuildkiteService.new
@service.stub(
project: @project,
service_hook: true,
- project_url: 'https://buildbox.io/account-name/example-project',
+ project_url: 'https://buildkite.com/account-name/example-project',
token: 'secret-sauce-webhook-token:secret-sauce-status-token'
)
end
@@ -44,7 +45,7 @@ describe BuildboxService do
describe :webhook_url do
it 'returns the webhook url' do
expect(@service.webhook_url).to eq(
- 'https://webhook.buildbox.io/deliver/secret-sauce-webhook-token'
+ 'https://webhook.buildkite.com/deliver/secret-sauce-webhook-token'
)
end
end
@@ -52,15 +53,15 @@ describe BuildboxService do
describe :commit_status_path do
it 'returns the correct status page' do
expect(@service.commit_status_path('2ab7834c')).to eq(
- 'https://gitlab.buildbox.io/status/secret-sauce-status-token.json?commit=2ab7834c'
+ 'https://gitlab.buildkite.com/status/secret-sauce-status-token.json?commit=2ab7834c'
)
end
end
describe :build_page do
it 'returns the correct build page' do
- expect(@service.build_page('2ab7834c')).to eq(
- 'https://buildbox.io/account-name/example-project/builds?commit=2ab7834c'
+ expect(@service.build_page('2ab7834c', nil)).to eq(
+ 'https://buildkite.com/account-name/example-project/builds?commit=2ab7834c'
)
end
end
@@ -68,14 +69,14 @@ describe BuildboxService do
describe :builds_page do
it 'returns the correct path to the builds page' do
expect(@service.builds_path).to eq(
- 'https://buildbox.io/account-name/example-project/builds?branch=default-brancho'
+ 'https://buildkite.com/account-name/example-project/builds?branch=default-brancho'
)
end
end
describe :status_img_path do
it 'returns the correct path to the status image' do
- expect(@service.status_img_path).to eq('https://badge.buildbox.io/secret-sauce-status-token.svg')
+ expect(@service.status_img_path).to eq('https://badge.buildkite.com/secret-sauce-status-token.svg')
end
end
end
diff --git a/spec/models/project_services/flowdock_service_spec.rb b/spec/models/project_services/flowdock_service_spec.rb
index 73f68301a34..e6e8fbba6a7 100644
--- a/spec/models/project_services/flowdock_service_spec.rb
+++ b/spec/models/project_services/flowdock_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
index d44064bbe6a..1a7765e5c2a 100644
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ b/spec/models/project_services/gemnasium_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb
index 8bfb19e524b..e5bf9125313 100644
--- a/spec/models/project_services/gitlab_ci_service_spec.rb
+++ b/spec/models/project_services/gitlab_ci_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
@@ -39,11 +40,34 @@ describe GitlabCiService do
end
describe :commit_status_path do
- it { expect(@service.commit_status_path("2ab7834c")).to eq("http://ci.gitlab.org/projects/2/commits/2ab7834c/status.json?token=verySecret")}
+ it { expect(@service.commit_status_path("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c/status.json?token=verySecret")}
+ it { expect(@service.commit_status_path("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232/status.json?token=verySecret")}
end
describe :build_page do
- it { expect(@service.build_page("2ab7834c")).to eq("http://ci.gitlab.org/projects/2/commits/2ab7834c")}
+ it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")}
+ it { expect(@service.build_page("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232")}
+ 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..e34ca09bffc 100644
--- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
@@ -31,6 +32,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 +41,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 +58,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/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index b9f2bee148d..bbaf54488be 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -5,15 +5,17 @@
# id :integer not null, primary key
# type :string(255)
# title :string(255)
-# project_id :integer not null
+# project_id :integer
# created_at :datetime
# updated_at :datetime
# active :boolean default(FALSE), not null
# properties :text
+# template :boolean default(FALSE)
# push_events :boolean default(TRUE)
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
@@ -63,7 +65,7 @@ describe HipchatService do
end
context 'tag_push events' do
- let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, '000000', '111111', 'refs/tags/test', []) }
+ let(:push_sample_data) { Gitlab::PushDataBuilder.build(project, user, Gitlab::Git::BLANK_SHA, '1' * 40, 'refs/tags/test', []) }
it "should call Hipchat API for tag push events" do
hipchat.execute(push_sample_data)
@@ -213,5 +215,21 @@ describe HipchatService do
"<pre>snippet note</pre>")
end
end
+
+ context "#message_options" do
+ it "should be set to the defaults" do
+ expect(hipchat.send(:message_options)).to eq({notify: false, color: 'yellow'})
+ end
+
+ it "should set notfiy to true" do
+ hipchat.stub(notify: '1')
+ expect(hipchat.send(:message_options)).to eq({notify: true, color: 'yellow'})
+ end
+
+ it "should set the color" do
+ hipchat.stub(color: 'red')
+ expect(hipchat.send(:message_options)).to eq({notify: false, color: 'red'})
+ end
+ end
end
end
diff --git a/spec/models/project_services/irker_service_spec.rb b/spec/models/project_services/irker_service_spec.rb
index d55399bc360..49face26bb4 100644
--- a/spec/models/project_services/irker_service_spec.rb
+++ b/spec/models/project_services/irker_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 355911e6377..ddd2cce212c 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/pushover_service_spec.rb b/spec/models/project_services/pushover_service_spec.rb
index 5a18fd09bfc..5f93703b50a 100644
--- a/spec/models/project_services/pushover_service_spec.rb
+++ b/spec/models/project_services/pushover_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb
index 3ef065459d8..10963481a12 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/slack_service/push_message_spec.rb
@@ -43,7 +43,7 @@ describe SlackService::PushMessage do
let(:args) {
{
after: 'after',
- before: '000000',
+ before: Gitlab::Git::BLANK_SHA,
project_name: 'project_name',
ref: 'refs/tags/new_tag',
user_name: 'user_name',
@@ -61,7 +61,7 @@ describe SlackService::PushMessage do
context 'new branch' do
before do
- args[:before] = '000000'
+ args[:before] = Gitlab::Git::BLANK_SHA
end
it 'returns a message regarding a new branch' do
@@ -75,7 +75,7 @@ describe SlackService::PushMessage do
context 'removed branch' do
before do
- args[:after] = '000000'
+ args[:after] = Gitlab::Git::BLANK_SHA
end
it 'returns a message regarding a removed branch' do
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index c36506644b3..e9105677d23 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 879a63dd9f9..37e21a90818 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -129,6 +129,48 @@ describe Project do
end
end
+ describe '#get_issue' do
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, project: project) }
+
+ context 'with default issues tracker' do
+ it 'returns an issue' do
+ expect(project.get_issue(issue.iid)).to eq issue
+ end
+
+ it 'returns nil when no issue found' do
+ expect(project.get_issue(999)).to be_nil
+ end
+ end
+
+ context 'with external issues tracker' do
+ before do
+ allow(project).to receive(:default_issues_tracker?).and_return(false)
+ end
+
+ it 'returns an ExternalIssue' do
+ issue = project.get_issue('FOO-1234')
+ expect(issue).to be_kind_of(ExternalIssue)
+ expect(issue.iid).to eq 'FOO-1234'
+ expect(issue.project).to eq project
+ end
+ end
+ end
+
+ describe '#issue_exists?' do
+ let(:project) { create(:empty_project) }
+
+ it 'is truthy when issue exists' do
+ expect(project).to receive(:get_issue).and_return(double)
+ expect(project.issue_exists?(1)).to be_truthy
+ end
+
+ it 'is falsey when issue does not exist' do
+ expect(project).to receive(:get_issue).and_return(nil)
+ expect(project.issue_exists?(1)).to be_falsey
+ end
+ end
+
describe :update_merge_requests do
let(:project) { create(:project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
@@ -180,25 +222,6 @@ describe Project do
end
end
- describe :issue_exists? do
- let(:project) { create(:project) }
- let(:existed_issue) { create(:issue, project: project) }
- let(:not_existed_issue) { create(:issue) }
- let(:ext_project) { create(:redmine_project) }
-
- it 'should be true or if used internal tracker and issue exists' do
- expect(project.issue_exists?(existed_issue.iid)).to be_truthy
- end
-
- it 'should be false or if used internal tracker and issue not exists' do
- expect(project.issue_exists?(not_existed_issue.iid)).to be_falsey
- end
-
- it 'should always be true if used other tracker' do
- expect(ext_project.issue_exists?(rand(100))).to be_truthy
- end
- end
-
describe :default_issues_tracker? do
let(:project) { create(:project) }
let(:ext_project) { create(:redmine_project) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index eeb0f3d9ee0..f41e5a97ca3 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -13,6 +13,13 @@ describe Repository do
it { is_expected.not_to include('fix') }
end
+ describe :tag_names_contains do
+ subject { repository.tag_names_contains(sample_commit.id) }
+
+ it { is_expected.to include('v1.1.0') }
+ it { is_expected.not_to include('v1.0.0') }
+ end
+
describe :last_commit_for_path do
subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id }
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 735652aea78..5d4827ce92a 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -15,6 +15,7 @@
# issues_events :boolean default(TRUE)
# merge_requests_events :boolean default(TRUE)
# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
#
require 'spec_helper'
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 10e90cae143..771709c127a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -49,11 +49,15 @@
# password_automatically_set :boolean default(FALSE)
# bitbucket_access_token :string(255)
# bitbucket_access_token_secret :string(255)
+# location :string(255)
+# public_email :string(255) default(""), not null
#
require 'spec_helper'
describe User do
+ include Gitlab::CurrentSettings
+
describe "Associations" do
it { is_expected.to have_one(:namespace) }
it { is_expected.to have_many(:snippets).class_name('Snippet').dependent(:destroy) }
@@ -112,6 +116,51 @@ describe User do
user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com")
expect(user).to be_invalid
end
+
+ context 'when no signup domains listed' do
+ before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) }
+ it 'accepts any email' do
+ user = build(:user, email: "info@example.com")
+ expect(user).to be_valid
+ end
+ end
+
+ context 'when a signup domain is listed and subdomains are allowed' do
+ before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) }
+ it 'accepts info@example.com' do
+ user = build(:user, email: "info@example.com")
+ expect(user).to be_valid
+ end
+
+ it 'accepts info@test.example.com' do
+ user = build(:user, email: "info@test.example.com")
+ expect(user).to be_valid
+ end
+
+ it 'rejects example@test.com' do
+ user = build(:user, email: "example@test.com")
+ expect(user).to be_invalid
+ end
+ end
+
+ context 'when a signup domain is listed and subdomains are not allowed' do
+ before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return(['example.com']) }
+
+ it 'accepts info@example.com' do
+ user = build(:user, email: "info@example.com")
+ expect(user).to be_valid
+ end
+
+ it 'rejects info@test.example.com' do
+ user = build(:user, email: "info@test.example.com")
+ expect(user).to be_invalid
+ end
+
+ it 'rejects example@test.com' do
+ user = build(:user, email: "example@test.com")
+ expect(user).to be_invalid
+ end
+ end
end
end
@@ -307,16 +356,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/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index f3fd805783f..fceb7668cac 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -78,6 +78,47 @@ describe WikiPage do
end
end
+ describe "dot in the title" do
+ let(:title) { 'Index v1.2.3' }
+
+ before do
+ @wiki_attr = {title: title, content: "Home Page", format: "markdown"}
+ end
+
+ describe "#create" do
+ after do
+ destroy_page(title)
+ end
+
+ context "with valid attributes" do
+ it "saves the wiki page" do
+ subject.create(@wiki_attr)
+ expect(wiki.find_page(title)).not_to be_nil
+ end
+
+ it "returns true" do
+ expect(subject.create(@wiki_attr)).to eq(true)
+ end
+ end
+ end
+
+ describe "#update" do
+ before do
+ create_page(title, "content")
+ @page = wiki.find_page(title)
+ end
+
+ it "updates the content of the page" do
+ @page.update("new content")
+ @page = wiki.find_page(title)
+ end
+
+ it "returns true" do
+ expect(@page.update("more content")).to be_truthy
+ end
+ end
+ end
+
describe "#update" do
before do
create_page("Update", "content")
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index fb3ff552c8d..7a784796031 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -50,7 +50,6 @@ describe API::API, api: true do
it 'should fail if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
expect(response.status).to eq(409)
- expect(json_response['message']['base']).to eq(['Invalid fork destination'])
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index d963dbac9f1..62b42d63fc2 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -3,8 +3,9 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
- let(:user1) { create(:user) }
+ let(:user1) { create(:user, can_create_group: false) }
let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
let(:admin) { create(:admin) }
let!(:group1) { create(:group) }
let!(:group2) { create(:group) }
@@ -94,32 +95,32 @@ describe API::API, api: true do
end
describe "POST /groups" do
- context "when authenticated as user" do
+ context "when authenticated as user without group permissions" do
it "should not create group" do
post api("/groups", user1), attributes_for(:group)
expect(response.status).to eq(403)
end
end
- context "when authenticated as admin" do
+ context "when authenticated as user with group permissions" do
it "should create group" do
- post api("/groups", admin), attributes_for(:group)
+ post api("/groups", user3), attributes_for(:group)
expect(response.status).to eq(201)
end
it "should not create group, duplicate" do
- post api("/groups", admin), {name: "Duplicate Test", path: group2.path}
+ post api("/groups", user3), {name: 'Duplicate Test', path: group2.path}
expect(response.status).to eq(400)
expect(response.message).to eq("Bad Request")
end
it "should return 400 bad request error if name not given" do
- post api("/groups", admin), {path: group2.path}
+ post api("/groups", user3), {path: group2.path}
expect(response.status).to eq(400)
end
it "should return 400 bad request error if path not given" do
- post api("/groups", admin), { name: 'test' }
+ post api("/groups", user3), {name: 'test'}
expect(response.status).to eq(400)
end
end
@@ -133,8 +134,8 @@ describe API::API, api: true do
end
it "should not remove a group if not an owner" do
- user3 = create(:user)
- group1.add_user(user3, Gitlab::Access::MASTER)
+ user4 = create(:user)
+ group1.add_user(user4, Gitlab::Access::MASTER)
delete api("/groups/#{group1.id}", user3)
expect(response.status).to eq(403)
end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index b6b0427debf..8770786f49a 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -194,6 +194,14 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(issue.iid)
end
+ it 'should return a project issue by iid' do
+ get api("/projects/#{project.id}/issues?iid=#{issue.iid}", user)
+ response.status.should == 200
+ json_response.first['title'].should == issue.title
+ json_response.first['id'].should == issue.id
+ json_response.first['iid'].should == issue.iid
+ end
+
it "should return 404 if issue id not found" do
get api("/projects/#{project.id}/issues/54321", user)
expect(response.status).to eq(404)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 9e252441a4f..dcd50f73326 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -115,6 +115,14 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(merge_request.iid)
end
+ it 'should return merge_request by iid' do
+ url = "/projects/#{project.id}/merge_requests?iid=#{merge_request.iid}"
+ get api(url, user)
+ response.status.should == 200
+ json_response.first['title'].should == merge_request.title
+ json_response.first['id'].should == merge_request.id
+ end
+
it "should return a 404 error if merge_request_id not found" do
get api("/projects/#{project.id}/merge_request/999", user)
expect(response.status).to eq(404)
@@ -312,6 +320,13 @@ describe API::API, api: true do
expect(json_response['message']).to eq('405 Method Not Allowed')
end
+ it "should return 405 if merge_request is a work in progress" do
+ merge_request.update_attribute(:title, "WIP: #{merge_request.title}")
+ put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user)
+ expect(response.status).to eq(405)
+ expect(json_response['message']).to eq('405 Method Not Allowed')
+ end
+
it "should return 401 if user has no permissions to merge" do
user2 = create(:user)
project.team << [user2, :reporter]
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index effb0723476..6890dd1f3a7 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -30,6 +30,13 @@ describe API::API, api: true do
expect(json_response['iid']).to eq(milestone.iid)
end
+ it 'should return a project milestone by iid' do
+ get api("/projects/#{project.id}/milestones?iid=#{milestone.iid}", user)
+ response.status.should == 200
+ json_response.first['title'].should == milestone.title
+ json_response.first['id'].should == milestone.id
+ end
+
it 'should return 401 error if user not authenticated' do
get api("/projects/#{project.id}/milestones/#{milestone.id}")
expect(response.status).to eq(401)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 0b3a47e3273..cc387378d3a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
+ include Gitlab::CurrentSettings
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:user3) { create(:user) }
@@ -56,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 }
@@ -202,6 +210,31 @@ describe API::API, api: true do
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+
+ context 'when a visibility level is restricted' do
+ before do
+ @project = attributes_for(:project, { public: true })
+ allow_any_instance_of(ApplicationSetting).to(
+ receive(:restricted_visibility_levels).and_return([20])
+ )
+ end
+
+ it 'should not allow a non-admin to use a restricted visibility level' do
+ post api('/projects', user), @project
+ expect(response.status).to eq(400)
+ expect(json_response['message']['visibility_level'].first).to(
+ match('restricted by your GitLab administrator')
+ )
+ end
+
+ it 'should allow an admin to override restricted visibility settings' do
+ post api('/projects', admin), @project
+ expect(json_response['public']).to be_truthy
+ expect(json_response['visibility_level']).to(
+ eq(Gitlab::VisibilityLevel::PUBLIC)
+ )
+ end
+ end
end
describe 'POST /projects/user/:id' do
@@ -221,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
@@ -399,7 +432,8 @@ describe API::API, api: true do
describe 'POST /projects/:id/snippets' do
it 'should create a new project snippet' do
post api("/projects/#{project.id}/snippets", user),
- title: 'api test', file_name: 'sample.rb', code: 'test'
+ title: 'api test', file_name: 'sample.rb', code: 'test',
+ visibility_level: '0'
expect(response.status).to eq(201)
expect(json_response['title']).to eq('api test')
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..327f3e6d23c 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -110,17 +110,22 @@ describe API::API, api: true do
end
it 'should return 400 error if name not given' do
- post api('/users', admin), email: 'test@example.com', password: 'pass1234'
+ post api('/users', admin), attributes_for(:user).except(:name)
expect(response.status).to eq(400)
end
it 'should return 400 error if password not given' do
- post api('/users', admin), email: 'test@example.com', name: 'test'
+ post api('/users', admin), attributes_for(:user).except(:password)
expect(response.status).to eq(400)
end
- it "should return 400 error if email not given" do
- post api('/users', admin), password: 'pass1234', name: 'test'
+ it 'should return 400 error if email not given' do
+ post api('/users', admin), attributes_for(:user).except(:email)
+ expect(response.status).to eq(400)
+ end
+
+ it 'should return 400 error if username not given' do
+ post api('/users', admin), attributes_for(:user).except(:username)
expect(response.status).to eq(400)
end
@@ -140,7 +145,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 +271,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 4308a765b56..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
@@ -338,17 +337,14 @@ describe Projects::CommitsController, 'routing' do
end
end
-# project_team_members GET /:project_id/team_members(.:format) team_members#index
-# POST /:project_id/team_members(.:format) team_members#create
-# new_project_team_member GET /:project_id/team_members/new(.:format) team_members#new
-# edit_project_team_member GET /:project_id/team_members/:id/edit(.:format) team_members#edit
-# project_team_member GET /:project_id/team_members/:id(.:format) team_members#show
-# PUT /:project_id/team_members/:id(.:format) team_members#update
-# DELETE /:project_id/team_members/:id(.:format) team_members#destroy
-describe Projects::TeamMembersController, 'routing' do
+# project_project_members GET /:project_id/project_members(.:format) project_members#index
+# POST /:project_id/project_members(.:format) project_members#create
+# PUT /:project_id/project_members/:id(.:format) project_members#update
+# DELETE /:project_id/project_members/:id(.:format) project_members#destroy
+describe Projects::ProjectMembersController, 'routing' do
it_behaves_like 'RESTful project resources' do
- let(:actions) { [:new, :create, :update, :destroy] }
- let(:controller) { 'team_members' }
+ let(:actions) { [:index, :create, :update, :destroy] }
+ let(:controller) { 'project_members' }
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index d4915b51952..953c8dd8ddc 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -28,7 +28,7 @@ end
# DELETE /snippets/:id(.:format) snippets#destroy
describe SnippetsController, "routing" do
it "to #user_index" do
- expect(get("/s/User")).to route_to('snippets#user_index', username: 'User')
+ expect(get("/s/User")).to route_to('snippets#index', username: 'User')
end
it "to #raw" do
@@ -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/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb
new file mode 100644
index 00000000000..08689c15ca8
--- /dev/null
+++ b/spec/services/create_snippet_service_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe CreateSnippetService do
+ before do
+ @user = create :user
+ @admin = create :user, admin: true
+ @opts = {
+ title: 'Test snippet',
+ file_name: 'snippet.rb',
+ content: 'puts "hello world"',
+ visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ }
+ end
+
+ context 'When public visibility is restricted' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to(
+ receive(:restricted_visibility_levels).and_return(
+ [Gitlab::VisibilityLevel::PUBLIC]
+ )
+ )
+
+ @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'non-admins should not be able to create a public snippet' do
+ snippet = create_snippet(nil, @user, @opts)
+ expect(snippet.errors.messages).to have_key(:visibility_level)
+ expect(snippet.errors.messages[:visibility_level].first).to(
+ match('Public visibility has been restricted')
+ )
+ end
+
+ it 'admins should be able to create a public snippet' do
+ snippet = create_snippet(nil, @admin, @opts)
+ expect(snippet.errors.any?).to be_falsey
+ expect(snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ def create_snippet(project, user, opts)
+ CreateSnippetService.new(project, user, opts).execute
+ end
+end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 1b1e3ca5f8b..e7558f28768 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -44,7 +44,7 @@ describe GitPushService do
before do
service.execute(project, user, @oldrev, @newrev, @ref)
@push_data = service.push_data
- @commit = project.repository.commit(@newrev)
+ @commit = project.commit(@newrev)
end
subject { @push_data }
@@ -145,18 +145,13 @@ describe GitPushService do
expect(project).to receive(:execute_hooks)
service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master')
end
-
- it "when pushing tags" do
- expect(project).not_to receive(:execute_hooks)
- service.execute(project, user, 'newrev', 'newrev', 'refs/tags/v1.0.0')
- end
end
end
describe "cross-reference notes" do
let(:issue) { create :issue, project: project }
let(:commit_author) { create :user }
- let(:commit) { project.repository.commit }
+ let(:commit) { project.commit }
before do
commit.stub({
@@ -169,22 +164,22 @@ describe GitPushService do
end
it "creates a note if a pushed commit mentions an issue" do
- expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project)
+ expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author)
service.execute(project, user, @oldrev, @newrev, @ref)
end
it "only creates a cross-reference note if one doesn't already exist" do
- Note.create_cross_reference_note(issue, commit, user, project)
+ Note.create_cross_reference_note(issue, commit, user)
- expect(Note).not_to receive(:create_cross_reference_note).with(issue, commit, commit_author, project)
+ expect(Note).not_to receive(:create_cross_reference_note).with(issue, commit, commit_author)
service.execute(project, user, @oldrev, @newrev, @ref)
end
it "defaults to the pushing user if the commit's author is not known" do
commit.stub(author_name: 'unknown name', author_email: 'unknown@email.com')
- expect(Note).to receive(:create_cross_reference_note).with(issue, commit, user, project)
+ expect(Note).to receive(:create_cross_reference_note).with(issue, commit, user)
service.execute(project, user, @oldrev, @newrev, @ref)
end
@@ -193,7 +188,7 @@ describe GitPushService do
allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([])
allow(project.repository).to receive(:commits_between).with("master", @newrev).and_return([commit])
- expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author, project)
+ expect(Note).to receive(:create_cross_reference_note).with(issue, commit, commit_author)
service.execute(project, user, @blankrev, @newrev, 'refs/heads/other')
end
@@ -203,7 +198,7 @@ describe GitPushService do
let(:issue) { create :issue, project: project }
let(:other_issue) { create :issue, project: project }
let(:commit_author) { create :user }
- let(:closing_commit) { project.repository.commit }
+ let(:closing_commit) { project.commit }
before do
closing_commit.stub({
@@ -239,5 +234,18 @@ describe GitPushService do
expect(Issue.find(issue.id)).to be_opened
end
end
-end
+ describe "empty project" do
+ let(:project) { create(:project_empty_repo) }
+ let(:new_ref) { 'refs/heads/feature'}
+
+ before do
+ allow(project).to receive(:default_branch).and_return('feature')
+ expect(project).to receive(:change_head) { 'feature'}
+ end
+
+ it 'push to first branch updates HEAD' do
+ service.execute(project, user, @blankrev, @newrev, new_ref)
+ end
+ end
+end
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index fcf462edbfc..76f69b396e0 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -1,32 +1,39 @@
require 'spec_helper'
describe GitTagPushService do
+ include RepoHelpers
+
let (:user) { create :user }
let (:project) { create :project }
let (:service) { GitTagPushService.new }
before do
- @ref = 'refs/tags/super-tag'
- @oldrev = 'b98a310def241a6fd9c9a9a3e7934c48e498fe81'
- @newrev = 'b19a04f53caeebf4fe5ec2327cb83e9253dc91bb'
+ @oldrev = Gitlab::Git::BLANK_SHA
+ @newrev = "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" # gitlab-test: git rev-parse refs/tags/v1.1.0
+ @ref = 'refs/tags/v1.1.0'
end
- describe 'Git Tag Push Data' do
+ describe "Git Tag Push Data" do
before do
service.execute(project, user, @oldrev, @newrev, @ref)
@push_data = service.push_data
+ @tag_name = Gitlab::Git.ref_name(@ref)
+ @tag = project.repository.find_tag(@tag_name)
+ @commit = project.commit(@tag.target)
end
subject { @push_data }
+ it { is_expected.to include(object_kind: 'tag_push') }
it { is_expected.to include(ref: @ref) }
it { is_expected.to include(before: @oldrev) }
it { is_expected.to include(after: @newrev) }
+ it { is_expected.to include(message: @tag.message) }
it { is_expected.to include(user_id: user.id) }
it { is_expected.to include(user_name: user.name) }
it { is_expected.to include(project_id: project.id) }
- context 'With repository data' do
+ context "with repository data" do
subject { @push_data[:repository] }
it { is_expected.to include(name: project.name) }
@@ -34,6 +41,41 @@ describe GitTagPushService do
it { is_expected.to include(description: project.description) }
it { is_expected.to include(homepage: project.web_url) }
end
+
+ context "with commits" do
+ subject { @push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { @push_data[:commits].first }
+
+ it { is_expected.to include(id: @commit.id) }
+ it { is_expected.to include(message: @commit.safe_message) }
+ it { is_expected.to include(timestamp: @commit.date.xmlschema) }
+ it do
+ is_expected.to include(
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ @commit.id
+ ].join('/')
+ )
+ end
+
+ context "with a author" do
+ subject { @push_data[:commits].first[:author] }
+
+ it { is_expected.to include(name: @commit.author_name) }
+ it { is_expected.to include(email: @commit.author_email) }
+ end
+ end
+ end
end
describe "Web Hooks" do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 2074f8e7f78..2a54b2e920a 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -41,18 +41,23 @@ describe NotificationService do
describe :new_note do
it do
+ add_users_with_subscription(note.project, issue)
+
should_email(@u_watcher.id)
should_email(note.noteable.author_id)
should_email(note.noteable.assignee_id)
should_email(@u_mentioned.id)
+ should_email(@subscriber.id)
should_not_email(note.author_id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
+ should_not_email(@unsubscriber.id)
+
notification.new_note(note)
end
it 'filters out "mentioned in" notes' do
- mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author, issue.project)
+ mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author)
expect(Notify).not_to receive(:note_issue_email)
notification.new_note(mentioned_note)
@@ -69,9 +74,9 @@ describe NotificationService do
user_project = note.project.project_members.find_by_user_id(@u_watcher.id)
user_project.notification_level = Notification::N_PARTICIPATING
user_project.save
- user_group = note.project.group.group_members.find_by_user_id(@u_watcher.id)
- user_group.notification_level = Notification::N_GLOBAL
- user_group.save
+ group_member = note.project.group.group_members.find_by_user_id(@u_watcher.id)
+ group_member.notification_level = Notification::N_GLOBAL
+ group_member.save
end
it do
@@ -123,7 +128,7 @@ describe NotificationService do
end
it 'filters out "mentioned in" notes' do
- mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author, issue.project)
+ mentioned_note = Note.create_cross_reference_note(mentioned_issue, issue, issue.author)
expect(Notify).not_to receive(:note_issue_email)
notification.new_note(mentioned_note)
@@ -144,7 +149,7 @@ describe NotificationService do
before do
build_team(note.project)
- note.stub(:commit_author => @u_committer)
+ allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
end
describe :new_note do
@@ -191,6 +196,7 @@ describe NotificationService do
before do
build_team(issue.project)
+ add_users_with_subscription(issue.project, issue)
end
describe :new_issue do
@@ -224,6 +230,8 @@ describe NotificationService do
should_email(issue.assignee_id)
should_email(@u_watcher.id)
should_email(@u_participant_mentioned.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
@@ -245,6 +253,8 @@ describe NotificationService do
should_email(issue.author_id)
should_email(@u_watcher.id)
should_email(@u_participant_mentioned.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
@@ -266,6 +276,8 @@ describe NotificationService do
should_email(issue.author_id)
should_email(@u_watcher.id)
should_email(@u_participant_mentioned.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
@@ -287,6 +299,7 @@ describe NotificationService do
before do
build_team(merge_request.target_project)
+ add_users_with_subscription(merge_request.target_project, merge_request)
end
describe :new_merge_request do
@@ -311,6 +324,8 @@ describe NotificationService do
it do
should_email(merge_request.assignee_id)
should_email(@u_watcher.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
notification.reassigned_merge_request(merge_request, merge_request.author)
@@ -329,6 +344,8 @@ describe NotificationService do
it do
should_email(merge_request.assignee_id)
should_email(@u_watcher.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
notification.close_mr(merge_request, @u_disabled)
@@ -347,6 +364,8 @@ describe NotificationService do
it do
should_email(merge_request.assignee_id)
should_email(@u_watcher.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
notification.merge_mr(merge_request, @u_disabled)
@@ -365,6 +384,8 @@ describe NotificationService do
it do
should_email(merge_request.assignee_id)
should_email(@u_watcher.id)
+ should_email(@subscriber.id)
+ should_not_email(@unsubscriber.id)
should_not_email(@u_participating.id)
should_not_email(@u_disabled.id)
notification.reopen_mr(merge_request, @u_disabled)
@@ -420,4 +441,15 @@ describe NotificationService do
project.team << [@u_mentioned, :master]
project.team << [@u_committer, :master]
end
+
+ def add_users_with_subscription(project, issuable)
+ @subscriber = create :user
+ @unsubscriber = create :user
+
+ project.team << [@subscriber, :master]
+ project.team << [@unsubscriber, :master]
+
+ issuable.subscriptions.create(user: @subscriber, subscribed: true)
+ issuable.subscriptions.create(user: @unsubscriber, subscribed: false)
+ end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 8bb48346202..337dae592dd 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -55,6 +55,33 @@ describe Projects::CreateService do
it { expect(File.exists?(@path)).to be_falsey }
end
end
+
+ context 'restricted visibility level' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to(
+ receive(:restricted_visibility_levels).and_return([20])
+ )
+
+ @opts.merge!(
+ visibility_level: Gitlab::VisibilityLevel.options['Public']
+ )
+ end
+
+ it 'should not allow a restricted visibility level for non-admins' do
+ project = create_project(@user, @opts)
+ expect(project).to respond_to(:errors)
+ expect(project.errors.messages).to have_key(:visibility_level)
+ expect(project.errors.messages[:visibility_level].first).to(
+ match('restricted by your GitLab administrator')
+ )
+ end
+
+ it 'should allow a restricted visibility level for admins' do
+ project = create_project(@admin, @opts)
+ expect(project.errors.any?).to be(false)
+ expect(project.saved?).to be(true)
+ end
+ end
end
def create_project(user, opts)
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index e55a2e3f8a0..f158ac87e2b 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -27,7 +27,7 @@ describe Projects::ForkService do
it "fails due to transaction failure" do
@to_project = fork_project(@from_project, @to_user, false)
expect(@to_project.errors).not_to be_empty
- expect(@to_project.errors[:base]).to include("Fork transaction failed.")
+ expect(@to_project.errors[:base]).to include("Failed to fork repository")
end
end
@@ -36,8 +36,19 @@ describe Projects::ForkService do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
expect(@existing_project.persisted?).to be_truthy
- expect(@to_project.errors[:base]).to include("Invalid fork destination")
- expect(@to_project.errors[:base]).not_to include("Fork transaction failed.")
+ expect(@to_project.errors[:name]).to eq(['has already been taken'])
+ expect(@to_project.errors[:path]).to eq(['has already been taken'])
+ 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
@@ -70,7 +81,7 @@ describe Projects::ForkService do
context 'fork project for group when user not owner' do
it 'group developer should fail to fork project into the group' do
to_project = fork_project(@project, @developer, true, @opts)
- expect(to_project.errors[:namespace]).to eq(['insufficient access rights'])
+ expect(to_project.errors[:namespace]).to eq(['is not valid'])
end
end
@@ -80,7 +91,6 @@ describe Projects::ForkService do
namespace: @group)
to_project = fork_project(@project, @group_owner, true, @opts)
expect(existing_project.persisted?).to be_truthy
- expect(to_project.errors[:base]).to eq(['Invalid fork destination'])
expect(to_project.errors[:name]).to eq(['has already been taken'])
expect(to_project.errors[:path]).to eq(['has already been taken'])
end
@@ -88,9 +98,7 @@ describe Projects::ForkService do
end
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)
- context.stub(gitlab_shell: shell)
- context.execute
+ allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(fork_success)
+ Projects::ForkService.new(from_project, user, params).execute
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index 10dbc548e86..ea5b8813105 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -47,9 +47,9 @@ describe Projects::UpdateService do
context 'respect configured visibility restrictions setting' do
before(:each) do
- @restrictions = double("restrictions")
- allow(@restrictions).to receive(:restricted_visibility_levels) { [ "public" ] }
- Settings.stub_chain(:gitlab).and_return(@restrictions)
+ allow_any_instance_of(ApplicationSetting).to(
+ receive(:restricted_visibility_levels).and_return([20])
+ )
end
context 'should be private when updated to private' do
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/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb
new file mode 100644
index 00000000000..841ef9bfed1
--- /dev/null
+++ b/spec/services/update_snippet_service_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe UpdateSnippetService do
+ before do
+ @user = create :user
+ @admin = create :user, admin: true
+ @opts = {
+ title: 'Test snippet',
+ file_name: 'snippet.rb',
+ content: 'puts "hello world"',
+ visibility_level: Gitlab::VisibilityLevel::PRIVATE
+ }
+ end
+
+ context 'When public visibility is restricted' do
+ before do
+ allow_any_instance_of(ApplicationSetting).to(
+ receive(:restricted_visibility_levels).and_return(
+ [Gitlab::VisibilityLevel::PUBLIC]
+ )
+ )
+
+ @snippet = create_snippet(@project, @user, @opts)
+ @opts.merge!(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it 'non-admins should not be able to update to public visibility' do
+ old_visibility = @snippet.visibility_level
+ update_snippet(@project, @user, @snippet, @opts)
+ expect(@snippet.errors.messages).to have_key(:visibility_level)
+ expect(@snippet.errors.messages[:visibility_level].first).to(
+ match('Public visibility has been restricted')
+ )
+ expect(@snippet.visibility_level).to eq(old_visibility)
+ end
+
+ it 'admins should be able to update to pubic visibility' do
+ old_visibility = @snippet.visibility_level
+ update_snippet(@project, @admin, @snippet, @opts)
+ expect(@snippet.visibility_level).not_to eq(old_visibility)
+ expect(@snippet.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ def create_snippet(project, user, opts)
+ CreateSnippetService.new(project, user, opts).execute
+ end
+
+ def update_snippet(project = nil, user, snippet, opts)
+ UpdateSnippetService.new(project, user, snippet, opts).execute
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index eaec2198dc8..8fe51cf4add 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -10,19 +10,13 @@ end
ENV["RAILS_ENV"] ||= 'test'
require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
-require 'capybara/rails'
-require 'capybara/rspec'
require 'webmock/rspec'
require 'email_spec'
require 'sidekiq/testing/inline'
-require 'capybara/poltergeist'
-
-Capybara.javascript_driver = :poltergeist
-Capybara.default_wait_time = 10
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
-Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
+Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
WebMock.disable_net_connect!(allow_localhost: true)
@@ -44,3 +38,5 @@ RSpec.configure do |config|
TestEnv.init
end
end
+
+ActiveRecord::Migration.maintain_test_schema!
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
new file mode 100644
index 00000000000..fed1ab6ee33
--- /dev/null
+++ b/spec/support/capybara.rb
@@ -0,0 +1,21 @@
+require 'capybara/rails'
+require 'capybara/rspec'
+require 'capybara/poltergeist'
+
+# Give CI some extra time
+timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10
+
+Capybara.javascript_driver = :poltergeist
+Capybara.register_driver :poltergeist do |app|
+ Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout)
+end
+
+Capybara.default_wait_time = timeout
+Capybara.ignore_hidden_elements = true
+
+unless ENV['CI'] || ENV['CI_SERVER']
+ require 'capybara-screenshot/rspec'
+
+ # Keep only the screenshots generated from the last failing test suite
+ Capybara::Screenshot.prune_strategy = :keep_last_run
+end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index 305592fa5a6..53fb6545553 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -1,23 +1,20 @@
# Specifications for behavior common to all Mentionable implementations.
# Requires a shared context containing:
-# - let(:subject) { "the mentionable implementation" }
+# - subject { "the mentionable implementation" }
# - let(:backref_text) { "the way that +subject+ should refer to itself in backreferences " }
# - let(:set_mentionable_text) { lambda { |txt| "block that assigns txt to the subject's mentionable_text" } }
def common_mentionable_setup
- # Avoid name collisions with let(:project) or let(:author) in the surrounding scope.
- let(:mproject) { create :project }
- let(:mauthor) { subject.author }
-
- let(:mentioned_issue) { create :issue, project: mproject }
- let(:other_issue) { create :issue, project: mproject }
- let(:mentioned_mr) { create :merge_request, :simple, source_project: mproject }
- let(:mentioned_commit) { double('commit', sha: '1234567890abcdef').as_null_object }
-
- let(:ext_proj) { create :project, :public }
- let(:ext_issue) { create :issue, project: ext_proj }
- let(:other_ext_issue) { create :issue, project: ext_proj }
- let(:ext_mr) { create :merge_request, :simple, source_project: ext_proj }
+ let(:project) { create :project }
+ let(:author) { subject.author }
+
+ let(:mentioned_issue) { create(:issue, project: project) }
+ let(:mentioned_mr) { create(:merge_request, :simple, source_project: project) }
+ let(:mentioned_commit) { project.repository.commit }
+
+ let(:ext_proj) { create(:project, :public) }
+ let(:ext_issue) { create(:issue, project: ext_proj) }
+ let(:ext_mr) { create(:merge_request, :simple, source_project: ext_proj) }
let(:ext_commit) { ext_proj.repository.commit }
# Override to add known commits to the repository stub.
@@ -26,20 +23,37 @@ def common_mentionable_setup
# A string that mentions each of the +mentioned_.*+ objects above. Mentionables should add a self-reference
# to this string and place it in their +mentionable_text+.
let(:ref_string) do
- "mentions ##{mentioned_issue.iid} twice ##{mentioned_issue.iid}, " +
- "!#{mentioned_mr.iid}, " +
- "#{ext_proj.path_with_namespace}##{ext_issue.iid}, " +
- "#{ext_proj.path_with_namespace}!#{ext_mr.iid}, " +
- "#{ext_proj.path_with_namespace}@#{ext_commit.short_id}, " +
- "#{mentioned_commit.sha[0..10]} and itself as #{backref_text}"
+ cross = ext_proj.path_with_namespace
+
+ <<-MSG.strip_heredoc
+ These references are new:
+ Issue: ##{mentioned_issue.iid}
+ Merge: !#{mentioned_mr.iid}
+ Commit: #{mentioned_commit.id}
+
+ This reference is a repeat and should only be mentioned once:
+ Repeat: ##{mentioned_issue.iid}
+
+ These references are cross-referenced:
+ Issue: #{cross}##{ext_issue.iid}
+ Merge: #{cross}!#{ext_mr.iid}
+ Commit: #{cross}@#{ext_commit.short_id}
+
+ This is a self-reference and should not be mentioned at all:
+ Self: #{backref_text}
+ MSG
end
before do
- # Wire the project's repository to return the mentioned commit, and +nil+ for any
- # unrecognized commits.
- commitmap = { '1234567890a' => mentioned_commit }
+ # Wire the project's repository to return the mentioned commit, and +nil+
+ # for any unrecognized commits.
+ commitmap = {
+ mentioned_commit.id => mentioned_commit
+ }
extra_commits.each { |c| commitmap[c.short_id] = c }
- allow(mproject.repository).to receive(:commit) { |sha| commitmap[sha] }
+
+ allow(project.repository).to receive(:commit) { |sha| commitmap[sha] }
+
set_mentionable_text.call(ref_string)
end
end
@@ -53,7 +67,7 @@ shared_examples 'a mentionable' do
it "extracts references from its reference property" do
# De-duplicate and omit itself
- refs = subject.references(mproject)
+ refs = subject.references(project)
expect(refs.size).to eq(6)
expect(refs).to include(mentioned_issue)
expect(refs).to include(mentioned_mr)
@@ -68,17 +82,18 @@ shared_examples 'a mentionable' do
ext_issue, ext_mr, ext_commit]
mentioned_objects.each do |referenced|
- expect(Note).to receive(:create_cross_reference_note).with(referenced, subject.local_reference, mauthor, mproject)
+ expect(Note).to receive(:create_cross_reference_note).
+ with(referenced, subject.local_reference, author)
end
- subject.create_cross_references!(mproject, mauthor)
+ subject.create_cross_references!(project, author)
end
it 'detects existing cross-references' do
- Note.create_cross_reference_note(mentioned_issue, subject.local_reference, mauthor, mproject)
+ Note.create_cross_reference_note(mentioned_issue, subject.local_reference, author)
- expect(subject.has_mentioned?(mentioned_issue)).to be_truthy
- expect(subject.has_mentioned?(mentioned_mr)).to be_falsey
+ expect(subject).to have_mentioned(mentioned_issue)
+ expect(subject).not_to have_mentioned(mentioned_mr)
end
end
@@ -87,29 +102,42 @@ shared_examples 'an editable mentionable' do
it_behaves_like 'a mentionable'
+ let(:new_issues) do
+ [create(:issue, project: project), create(:issue, project: ext_proj)]
+ end
+
it 'creates new cross-reference notes when the mentionable text is edited' do
- new_text = "still mentions ##{mentioned_issue.iid}, " +
- "#{mentioned_commit.sha[0..10]}, " +
- "#{ext_issue.iid}, " +
- "new refs: ##{other_issue.iid}, " +
- "#{ext_proj.path_with_namespace}##{other_ext_issue.iid}"
+ subject.save
+
+ cross = ext_proj.path_with_namespace
+ new_text = <<-MSG
+ These references already existed:
+ Issue: ##{mentioned_issue.iid}
+ Commit: #{mentioned_commit.id}
+
+ This cross-project reference already existed:
+ Issue: #{cross}##{ext_issue.iid}
+
+ These two references are introduced in an edit:
+ Issue: ##{new_issues[0].iid}
+ Cross: #{cross}##{new_issues[1].iid}
+ MSG
+
+ # These three objects were already referenced, and should not receive new
+ # notes
[mentioned_issue, mentioned_commit, ext_issue].each do |oldref|
- expect(Note).not_to receive(:create_cross_reference_note).with(oldref, subject.local_reference,
- mauthor, mproject)
+ expect(Note).not_to receive(:create_cross_reference_note).
+ with(oldref, any_args)
end
- [other_issue, other_ext_issue].each do |newref|
- expect(Note).to receive(:create_cross_reference_note).with(
- newref,
- subject.local_reference,
- mauthor,
- mproject
- )
+ # These two issues are new and should receive reference notes
+ new_issues.each do |newref|
+ expect(Note).to receive(:create_cross_reference_note).
+ with(newref, subject.local_reference, author)
end
- subject.save
set_mentionable_text.call(new_text)
- subject.notice_added_references(mproject, mauthor)
+ subject.notice_added_references(project, author)
end
end
diff --git a/spec/support/reference_filter_spec_helper.rb b/spec/support/reference_filter_spec_helper.rb
new file mode 100644
index 00000000000..06c39e1ada5
--- /dev/null
+++ b/spec/support/reference_filter_spec_helper.rb
@@ -0,0 +1,50 @@
+# Common methods and setup for Gitlab::Markdown reference filter specs
+#
+# Must be included into specs manually
+module ReferenceFilterSpecHelper
+ extend ActiveSupport::Concern
+
+ # Shortcut to Rails' auto-generated routes helpers, to avoid including the
+ # module
+ def urls
+ Rails.application.routes.url_helpers
+ end
+
+ # Perform `call` on the described class
+ #
+ # Automatically passes the current `project` value to the context if none is
+ # provided.
+ #
+ # html - String text to pass to the filter's `call` method.
+ # contexts - Hash context for the filter. (default: {project: project})
+ #
+ # Returns the String text returned by the filter's `call` method.
+ def filter(html, contexts = {})
+ contexts.reverse_merge!(project: project)
+ described_class.call(html, contexts)
+ end
+
+ # Run text through HTML::Pipeline with the current filter and return the
+ # result Hash
+ #
+ # body - String text to run through the pipeline
+ # contexts - Hash context for the filter. (default: {project: project})
+ #
+ # Returns the Hash of the pipeline result
+ def pipeline_result(body, contexts = {})
+ contexts.reverse_merge!(project: project)
+
+ pipeline = HTML::Pipeline.new([described_class], contexts)
+ pipeline.call(body)
+ end
+
+ def allow_cross_reference!
+ allow_any_instance_of(described_class).
+ to receive(:user_can_reference_project?).and_return(true)
+ end
+
+ def disallow_cross_reference!
+ allow_any_instance_of(described_class).
+ to receive(:user_can_reference_project?).and_return(false)
+ end
+end
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index 4c4775da692..aadf791bf3f 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -43,6 +43,25 @@ eos
)
end
+ def another_sample_commit
+ OpenStruct.new(
+ id: "e56497bb5f03a90a51293fc6d516788730953899",
+ parent_id: '4cd80ccab63c82b4bad16faa5193fbd2aa06df40',
+ author_full_name: "Sytse Sijbrandij",
+ author_email: "sytse@gitlab.com",
+ files_changed_count: 1,
+ message: <<eos
+Add directory structure for tree_helper spec
+
+This directory structure is needed for a testing the method flatten_tree(tree) in the TreeHelper module
+
+See [merge request #275](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/275#note_732774)
+
+See merge request !2
+eos
+ )
+ end
+
def sample_big_commit
OpenStruct.new(
id: "913c66a37b4a45b9769037c55c2d238bd0942d2e",
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/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index 490f453d468..927c72c7409 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -1,42 +1,32 @@
# Specs for task state functionality for issues and merge requests.
#
# Requires a context containing:
-# let(:subject) { Issue or MergeRequest }
+# subject { Issue or MergeRequest }
shared_examples 'a Taskable' do
before do
- subject.description = <<EOT.gsub(/ {6}/, '')
+ subject.description = <<-EOT.strip_heredoc
* [ ] Task 1
* [x] Task 2
* [x] Task 3
* [ ] Task 4
* [ ] Task 5
-EOT
- end
-
- it 'updates the Nth task correctly' do
- subject.update_nth_task(1, true)
- expect(subject.description).to match(/\[x\] Task 1/)
-
- subject.update_nth_task(2, true)
- expect(subject.description).to match('\[x\] Task 2')
-
- subject.update_nth_task(3, false)
- expect(subject.description).to match('\[ \] Task 3')
-
- subject.update_nth_task(4, false)
- expect(subject.description).to match('\[ \] Task 4')
+ EOT
end
it 'returns the correct task status' do
expect(subject.task_status).to match('5 tasks')
- expect(subject.task_status).to match('2 done')
- expect(subject.task_status).to match('3 unfinished')
+ expect(subject.task_status).to match('2 completed')
+ expect(subject.task_status).to match('3 remaining')
end
- it 'knows if it has tasks' do
- expect(subject.tasks?).to be_truthy
+ describe '#tasks?' do
+ it 'returns true when object has tasks' do
+ expect(subject.tasks?).to eq true
+ end
- subject.description = 'Now I have no tasks'
- expect(subject.tasks?).to be_falsey
+ it 'returns false when object has no tasks' do
+ subject.description = 'Now I have no tasks'
+ expect(subject.tasks?).to eq false
+ end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index f869488d8d8..6d4a8067910 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -14,6 +14,10 @@ module TestEnv
'master' => '5937ac0'
}
+ FORKED_BRANCH_SHA = BRANCH_SHA.merge({
+ 'add-submodule-version-bump' => '3f547c08'
+ })
+
# Test environment
#
# See gitlab.yml.example test section for paths
@@ -31,6 +35,9 @@ module TestEnv
# Create repository for FactoryGirl.create(:project)
setup_factory_repo
+
+ # Create repository for FactoryGirl.create(:forked_project_with_submodules)
+ setup_forked_repo
end
def disable_mailer
@@ -48,7 +55,7 @@ module TestEnv
tmp_test_path = Rails.root.join('tmp', 'tests', '**')
Dir[tmp_test_path].each do |entry|
- unless File.basename(entry) =~ /\Agitlab-(shell|test)\z/
+ unless File.basename(entry) =~ /\Agitlab-(shell|test|test-fork)\z/
FileUtils.rm_rf(entry)
end
end
@@ -61,14 +68,26 @@ module TestEnv
end
def setup_factory_repo
- clone_url = "https://gitlab.com/gitlab-org/#{factory_repo_name}.git"
+ setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
+ BRANCH_SHA)
+ end
+
+ # This repo has a submodule commit that is not present in the main test
+ # repository.
+ def setup_forked_repo
+ setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name,
+ FORKED_BRANCH_SHA)
+ end
+
+ def setup_repo(repo_path, repo_path_bare, repo_name, branch_sha)
+ clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git"
- unless File.directory?(factory_repo_path)
- system(*%W(git clone -q #{clone_url} #{factory_repo_path}))
+ unless File.directory?(repo_path)
+ system(*%W(git clone -q #{clone_url} #{repo_path}))
end
- Dir.chdir(factory_repo_path) do
- BRANCH_SHA.each do |branch, sha|
+ Dir.chdir(repo_path) do
+ branch_sha.each do |branch, sha|
# Try to reset without fetching to avoid using the network.
reset = %W(git update-ref refs/heads/#{branch} #{sha})
unless system(*reset)
@@ -85,7 +104,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 #{repo_path} #{repo_path_bare}))
end
def copy_repo(project)
@@ -100,6 +119,14 @@ module TestEnv
Gitlab.config.gitlab_shell.repos_path
end
+ def copy_forked_repo_with_submodules(project)
+ base_repo_path = File.expand_path(forked_repo_path_bare)
+ target_repo_path = File.expand_path(repos_path + "/#{project.namespace.path}/#{project.path}.git")
+ FileUtils.mkdir_p(target_repo_path)
+ FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
+ FileUtils.chmod_R 0755, target_repo_path
+ end
+
private
def factory_repo_path
@@ -113,4 +140,23 @@ module TestEnv
def factory_repo_name
'gitlab-test'
end
+
+ def forked_repo_path
+ @forked_repo_path ||= Rails.root.join('tmp', 'tests', forked_repo_name)
+ end
+
+ def forked_repo_path_bare
+ "#{forked_repo_path}_bare"
+ end
+
+ def forked_repo_name
+ 'gitlab-test-fork'
+ 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 60942cc95fc..a59f74c2121 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -10,17 +10,17 @@ describe 'gitlab:app namespace rake task' do
Rake::Task.define_task :environment
end
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+
describe 'backup_restore' do
before do
# avoid writing task output to spec progress
allow($stdout).to receive :write
end
- let :run_rake_task do
- Rake::Task["gitlab:backup:restore"].reenable
- Rake.application.invoke_task "gitlab:backup:restore"
- end
-
context 'gitlab version' do
before do
Dir.stub glob: []
@@ -36,7 +36,9 @@ describe 'gitlab:app namespace rake task' do
it 'should fail on mismatch' do
YAML.stub load_file: {gitlab_version: "not #{gitlab_version}" }
- expect { run_rake_task }.to raise_error SystemExit
+ expect { run_rake_task('gitlab:backup:restore') }.to(
+ raise_error SystemExit
+ )
end
it 'should invoke restoration on mach' do
@@ -44,9 +46,107 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:repo:restore"]).to receive :invoke
expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
- expect { run_rake_task }.to_not raise_error
+ expect { run_rake_task('gitlab:backup:restore') }.to_not raise_error
end
end
end # backup_restore task
+
+ describe 'backup_create' do
+ def tars_glob
+ Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
+ end
+
+ before :all do
+ # 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
+ run_rake_task('gitlab:backup:create')
+ $stdout = orig_stdout
+
+ @backup_tar = (tars_glob - existing_tars).first
+ end
+
+ after :all do
+ FileUtils.rm(@backup_tar)
+ end
+
+ it 'should set correct permissions on the tar file' do
+ expect(File.exist?(@backup_tar)).to be_truthy
+ expect(File::Stat.new(@backup_tar).mode.to_s(8)).to eq('100600')
+ end
+
+ it 'should set correct permissions on the tar contents' do
+ tar_contents, exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar} db uploads repositories}
+ )
+ expect(exit_status).to eq(0)
+ 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].* (db|uploads|repositories)\/$/)
+ end
+
+ it 'should delete temp directories' do
+ temp_dirs = Dir.glob(
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads}')
+ )
+
+ 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/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 8eabc46112b..df1a2b84a53 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -1,6 +1,10 @@
require 'spec_helper'
describe PostReceive do
+ let(:changes) { "123456 789012 refs/heads/tést\n654321 210987 refs/tags/tag" }
+ let(:wrongly_encoded_changes) { changes.encode("ISO-8859-1").force_encoding("UTF-8") }
+ let(:base64_changes) { Base64.encode64(wrongly_encoded_changes) }
+
context "as a resque worker" do
it "reponds to #perform" do
expect(PostReceive.new).to respond_to(:perform)
@@ -14,7 +18,7 @@ describe PostReceive do
it "fetches the correct project" do
expect(Project).to receive(:find_with_namespace).with(project.path_with_namespace).and_return(project)
- PostReceive.new.perform(pwd(project), key_id, changes)
+ PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
it "does not run if the author is not in the project" do
@@ -22,24 +26,20 @@ describe PostReceive do
expect(project).not_to receive(:execute_hooks)
- expect(PostReceive.new.perform(pwd(project), key_id, changes)).to be_falsey
+ expect(PostReceive.new.perform(pwd(project), key_id, base64_changes)).to be_falsey
end
it "asks the project to trigger all hooks" do
Project.stub(find_with_namespace: project)
- expect(project).to receive(:execute_hooks)
- expect(project).to receive(:execute_services)
+ expect(project).to receive(:execute_hooks).twice
+ expect(project).to receive(:execute_services).twice
expect(project).to receive(:update_merge_requests)
- PostReceive.new.perform(pwd(project), key_id, changes)
+ PostReceive.new.perform(pwd(project), key_id, base64_changes)
end
end
def pwd(project)
File.join(Gitlab.config.gitlab_shell.repos_path, project.path_with_namespace)
end
-
- def changes
- 'd14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/master'
- 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
diff --git a/vendor/assets/javascripts/jasmine-fixture.js b/vendor/assets/javascripts/jasmine-fixture.js
new file mode 100755
index 00000000000..9980aec6ddb
--- /dev/null
+++ b/vendor/assets/javascripts/jasmine-fixture.js
@@ -0,0 +1,433 @@
+/* jasmine-fixture - 1.3.1
+ * Makes injecting HTML snippets into the DOM easy & clean!
+ * https://github.com/searls/jasmine-fixture
+ */
+(function() {
+ var createHTMLBlock,
+ __slice = [].slice;
+
+ (function($) {
+ var ewwSideEffects, jasmineFixture, originalAffix, originalJasmineDotFixture, originalJasmineFixture, root, _, _ref;
+ root = (1, eval)('this');
+ originalJasmineFixture = root.jasmineFixture;
+ originalJasmineDotFixture = (_ref = root.jasmine) != null ? _ref.fixture : void 0;
+ originalAffix = root.affix;
+ _ = function(list) {
+ return {
+ inject: function(iterator, memo) {
+ var item, _i, _len, _results;
+ _results = [];
+ for (_i = 0, _len = list.length; _i < _len; _i++) {
+ item = list[_i];
+ _results.push(memo = iterator(memo, item));
+ }
+ return _results;
+ }
+ };
+ };
+ root.jasmineFixture = function($) {
+ var $whatsTheRootOf, affix, create, jasmineFixture, noConflict;
+ affix = function(selectorOptions) {
+ return create.call(this, selectorOptions, true);
+ };
+ create = function(selectorOptions, attach) {
+ var $top;
+ $top = null;
+ _(selectorOptions.split(/[ ](?![^\{]*\})(?=[^\]]*?(?:\[|$))/)).inject(function($parent, elementSelector) {
+ var $el;
+ if (elementSelector === ">") {
+ return $parent;
+ }
+ $el = createHTMLBlock($, elementSelector);
+ if (attach || $top) {
+ $el.appendTo($parent);
+ }
+ $top || ($top = $el);
+ return $el;
+ }, $whatsTheRootOf(this));
+ return $top;
+ };
+ noConflict = function() {
+ var currentJasmineFixture, _ref1;
+ currentJasmineFixture = jasmine.fixture;
+ root.jasmineFixture = originalJasmineFixture;
+ if ((_ref1 = root.jasmine) != null) {
+ _ref1.fixture = originalJasmineDotFixture;
+ }
+ root.affix = originalAffix;
+ return currentJasmineFixture;
+ };
+ $whatsTheRootOf = function(that) {
+ if (that.jquery != null) {
+ return that;
+ } else if ($('#jasmine_content').length > 0) {
+ return $('#jasmine_content');
+ } else {
+ return $('<div id="jasmine_content"></div>').appendTo('body');
+ }
+ };
+ jasmineFixture = {
+ affix: affix,
+ create: create,
+ noConflict: noConflict
+ };
+ ewwSideEffects(jasmineFixture);
+ return jasmineFixture;
+ };
+ ewwSideEffects = function(jasmineFixture) {
+ var _ref1;
+ if ((_ref1 = root.jasmine) != null) {
+ _ref1.fixture = jasmineFixture;
+ }
+ $.fn.affix = root.affix = jasmineFixture.affix;
+ return afterEach(function() {
+ return $('#jasmine_content').remove();
+ });
+ };
+ if ($) {
+ return jasmineFixture = root.jasmineFixture($);
+ } else {
+ return root.affix = function() {
+ var nowJQueryExists;
+ nowJQueryExists = window.jQuery || window.$;
+ if (nowJQueryExists != null) {
+ jasmineFixture = root.jasmineFixture(nowJQueryExists);
+ return affix.call.apply(affix, [this].concat(__slice.call(arguments)));
+ } else {
+ throw new Error("jasmine-fixture requires jQuery to be defined at window.jQuery or window.$");
+ }
+ };
+ }
+ })(window.jQuery || window.$);
+
+ createHTMLBlock = (function() {
+ var bindData, bindEvents, parseAttributes, parseClasses, parseContents, parseEnclosure, parseReferences, parseVariableScope, regAttr, regAttrDfn, regAttrs, regCBrace, regClass, regClasses, regData, regDatas, regEvent, regEvents, regExclamation, regId, regReference, regTag, regTagNotContent, regZenTagDfn;
+ createHTMLBlock = function($, ZenObject, data, functions, indexes) {
+ var ZenCode, arr, block, blockAttrs, blockClasses, blockHTML, blockId, blockTag, blocks, el, el2, els, forScope, indexName, inner, len, obj, origZenCode, paren, result, ret, zc, zo;
+ if ($.isPlainObject(ZenObject)) {
+ ZenCode = ZenObject.main;
+ } else {
+ ZenCode = ZenObject;
+ ZenObject = {
+ main: ZenCode
+ };
+ }
+ origZenCode = ZenCode;
+ if (indexes === undefined) {
+ indexes = {};
+ }
+ if (ZenCode.charAt(0) === "!" || $.isArray(data)) {
+ if ($.isArray(data)) {
+ forScope = ZenCode;
+ } else {
+ obj = parseEnclosure(ZenCode, "!");
+ obj = obj.substring(obj.indexOf(":") + 1, obj.length - 1);
+ forScope = parseVariableScope(ZenCode);
+ }
+ while (forScope.charAt(0) === "@") {
+ forScope = parseVariableScope("!for:!" + parseReferences(forScope, ZenObject));
+ }
+ zo = ZenObject;
+ zo.main = forScope;
+ el = $();
+ if (ZenCode.substring(0, 5) === "!for:" || $.isArray(data)) {
+ if (!$.isArray(data) && obj.indexOf(":") > 0) {
+ indexName = obj.substring(0, obj.indexOf(":"));
+ obj = obj.substr(obj.indexOf(":") + 1);
+ }
+ arr = ($.isArray(data) ? data : data[obj]);
+ zc = zo.main;
+ if ($.isArray(arr) || $.isPlainObject(arr)) {
+ $.map(arr, function(value, index) {
+ var next;
+ zo.main = zc;
+ if (indexName !== undefined) {
+ indexes[indexName] = index;
+ }
+ if (!$.isPlainObject(value)) {
+ value = {
+ value: value
+ };
+ }
+ next = createHTMLBlock($, zo, value, functions, indexes);
+ if (el.length !== 0) {
+ return $.each(next, function(index, value) {
+ return el.push(value);
+ });
+ }
+ });
+ }
+ if (!$.isArray(data)) {
+ ZenCode = ZenCode.substr(obj.length + 6 + forScope.length);
+ } else {
+ ZenCode = "";
+ }
+ } else if (ZenCode.substring(0, 4) === "!if:") {
+ result = parseContents("!" + obj + "!", data, indexes);
+ if (result !== "undefined" || result !== "false" || result !== "") {
+ el = createHTMLBlock($, zo, data, functions, indexes);
+ }
+ ZenCode = ZenCode.substr(obj.length + 5 + forScope.length);
+ }
+ ZenObject.main = ZenCode;
+ } else if (ZenCode.charAt(0) === "(") {
+ paren = parseEnclosure(ZenCode, "(", ")");
+ inner = paren.substring(1, paren.length - 1);
+ ZenCode = ZenCode.substr(paren.length);
+ zo = ZenObject;
+ zo.main = inner;
+ el = createHTMLBlock($, zo, data, functions, indexes);
+ } else {
+ blocks = ZenCode.match(regZenTagDfn);
+ block = blocks[0];
+ if (block.length === 0) {
+ return "";
+ }
+ if (block.indexOf("@") >= 0) {
+ ZenCode = parseReferences(ZenCode, ZenObject);
+ zo = ZenObject;
+ zo.main = ZenCode;
+ return createHTMLBlock($, zo, data, functions, indexes);
+ }
+ block = parseContents(block, data, indexes);
+ blockClasses = parseClasses($, block);
+ if (regId.test(block)) {
+ blockId = regId.exec(block)[1];
+ }
+ blockAttrs = parseAttributes(block, data);
+ blockTag = (block.charAt(0) === "{" ? "span" : "div");
+ if (ZenCode.charAt(0) !== "#" && ZenCode.charAt(0) !== "." && ZenCode.charAt(0) !== "{") {
+ blockTag = regTag.exec(block)[1];
+ }
+ if (block.search(regCBrace) !== -1) {
+ blockHTML = block.match(regCBrace)[1];
+ }
+ blockAttrs = $.extend(blockAttrs, {
+ id: blockId,
+ "class": blockClasses,
+ html: blockHTML
+ });
+ el = $("<" + blockTag + ">", blockAttrs);
+ el.attr(blockAttrs);
+ el = bindEvents(block, el, functions);
+ el = bindData(block, el, data);
+ ZenCode = ZenCode.substr(blocks[0].length);
+ ZenObject.main = ZenCode;
+ }
+ if (ZenCode.length > 0) {
+ if (ZenCode.charAt(0) === ">") {
+ if (ZenCode.charAt(1) === "(") {
+ zc = parseEnclosure(ZenCode.substr(1), "(", ")");
+ ZenCode = ZenCode.substr(zc.length + 1);
+ } else if (ZenCode.charAt(1) === "!") {
+ obj = parseEnclosure(ZenCode.substr(1), "!");
+ forScope = parseVariableScope(ZenCode.substr(1));
+ zc = obj + forScope;
+ ZenCode = ZenCode.substr(zc.length + 1);
+ } else {
+ len = Math.max(ZenCode.indexOf("+"), ZenCode.length);
+ zc = ZenCode.substring(1, len);
+ ZenCode = ZenCode.substr(len);
+ }
+ zo = ZenObject;
+ zo.main = zc;
+ els = $(createHTMLBlock($, zo, data, functions, indexes));
+ els.appendTo(el);
+ }
+ if (ZenCode.charAt(0) === "+") {
+ zo = ZenObject;
+ zo.main = ZenCode.substr(1);
+ el2 = createHTMLBlock($, zo, data, functions, indexes);
+ $.each(el2, function(index, value) {
+ return el.push(value);
+ });
+ }
+ }
+ ret = el;
+ return ret;
+ };
+ bindData = function(ZenCode, el, data) {
+ var datas, i, split;
+ if (ZenCode.search(regDatas) === 0) {
+ return el;
+ }
+ datas = ZenCode.match(regDatas);
+ if (datas === null) {
+ return el;
+ }
+ i = 0;
+ while (i < datas.length) {
+ split = regData.exec(datas[i]);
+ if (split[3] === undefined) {
+ $(el).data(split[1], data[split[1]]);
+ } else {
+ $(el).data(split[1], data[split[3]]);
+ }
+ i++;
+ }
+ return el;
+ };
+ bindEvents = function(ZenCode, el, functions) {
+ var bindings, fn, i, split;
+ if (ZenCode.search(regEvents) === 0) {
+ return el;
+ }
+ bindings = ZenCode.match(regEvents);
+ if (bindings === null) {
+ return el;
+ }
+ i = 0;
+ while (i < bindings.length) {
+ split = regEvent.exec(bindings[i]);
+ if (split[2] === undefined) {
+ fn = functions[split[1]];
+ } else {
+ fn = functions[split[2]];
+ }
+ $(el).bind(split[1], fn);
+ i++;
+ }
+ return el;
+ };
+ parseAttributes = function(ZenBlock, data) {
+ var attrStrs, attrs, i, parts;
+ if (ZenBlock.search(regAttrDfn) === -1) {
+ return undefined;
+ }
+ attrStrs = ZenBlock.match(regAttrDfn);
+ attrs = {};
+ i = 0;
+ while (i < attrStrs.length) {
+ parts = regAttr.exec(attrStrs[i]);
+ attrs[parts[1]] = "";
+ if (parts[3] !== undefined) {
+ attrs[parts[1]] = parseContents(parts[3], data);
+ }
+ i++;
+ }
+ return attrs;
+ };
+ parseClasses = function($, ZenBlock) {
+ var classes, clsString, i;
+ ZenBlock = ZenBlock.match(regTagNotContent)[0];
+ if (ZenBlock.search(regClasses) === -1) {
+ return undefined;
+ }
+ classes = ZenBlock.match(regClasses);
+ clsString = "";
+ i = 0;
+ while (i < classes.length) {
+ clsString += " " + regClass.exec(classes[i])[1];
+ i++;
+ }
+ return $.trim(clsString);
+ };
+ parseContents = function(ZenBlock, data, indexes) {
+ var html;
+ if (indexes === undefined) {
+ indexes = {};
+ }
+ html = ZenBlock;
+ if (data === undefined) {
+ return html;
+ }
+ while (regExclamation.test(html)) {
+ html = html.replace(regExclamation, function(str, str2) {
+ var begChar, fn, val;
+ begChar = "";
+ if (str.indexOf("!for:") > 0 || str.indexOf("!if:") > 0) {
+ return str;
+ }
+ if (str.charAt(0) !== "!") {
+ begChar = str.charAt(0);
+ str = str.substring(2, str.length - 1);
+ }
+ fn = new Function("data", "indexes", "var r=undefined;" + "with(data){try{r=" + str + ";}catch(e){}}" + "with(indexes){try{if(r===undefined)r=" + str + ";}catch(e){}}" + "return r;");
+ val = unescape(fn(data, indexes));
+ return begChar + val;
+ });
+ }
+ html = html.replace(/\\./g, function(str) {
+ return str.charAt(1);
+ });
+ return unescape(html);
+ };
+ parseEnclosure = function(ZenCode, open, close, count) {
+ var index, ret;
+ if (close === undefined) {
+ close = open;
+ }
+ index = 1;
+ if (count === undefined) {
+ count = (ZenCode.charAt(0) === open ? 1 : 0);
+ }
+ if (count === 0) {
+ return;
+ }
+ while (count > 0 && index < ZenCode.length) {
+ if (ZenCode.charAt(index) === close && ZenCode.charAt(index - 1) !== "\\") {
+ count--;
+ } else {
+ if (ZenCode.charAt(index) === open && ZenCode.charAt(index - 1) !== "\\") {
+ count++;
+ }
+ }
+ index++;
+ }
+ ret = ZenCode.substring(0, index);
+ return ret;
+ };
+ parseReferences = function(ZenCode, ZenObject) {
+ ZenCode = ZenCode.replace(regReference, function(str) {
+ var fn;
+ str = str.substr(1);
+ fn = new Function("objs", "var r=\"\";" + "with(objs){try{" + "r=" + str + ";" + "}catch(e){}}" + "return r;");
+ return fn(ZenObject, parseReferences);
+ });
+ return ZenCode;
+ };
+ parseVariableScope = function(ZenCode) {
+ var forCode, rest, tag;
+ if (ZenCode.substring(0, 5) !== "!for:" && ZenCode.substring(0, 4) !== "!if:") {
+ return undefined;
+ }
+ forCode = parseEnclosure(ZenCode, "!");
+ ZenCode = ZenCode.substr(forCode.length);
+ if (ZenCode.charAt(0) === "(") {
+ return parseEnclosure(ZenCode, "(", ")");
+ }
+ tag = ZenCode.match(regZenTagDfn)[0];
+ ZenCode = ZenCode.substr(tag.length);
+ if (ZenCode.length === 0 || ZenCode.charAt(0) === "+") {
+ return tag;
+ } else if (ZenCode.charAt(0) === ">") {
+ rest = "";
+ rest = parseEnclosure(ZenCode.substr(1), "(", ")", 1);
+ return tag + ">" + rest;
+ }
+ return undefined;
+ };
+ regZenTagDfn = /([#\.\@]?[\w-]+|\[([\w-!?=:"']+(="([^"]|\\")+")? {0,})+\]|\~[\w$]+=[\w$]+|&[\w$]+(=[\w$]+)?|[#\.\@]?!([^!]|\\!)+!){0,}(\{([^\}]|\\\})+\})?/i;
+ regTag = /(\w+)/i;
+ regId = /(?:^|\b)#([\w-!]+)/i;
+ regTagNotContent = /((([#\.]?[\w-]+)?(\[([\w!]+(="([^"]|\\")+")? {0,})+\])?)+)/i;
+ /*
+ See lookahead syntax (?!) at https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp
+ */
+
+ regClasses = /(\.[\w-]+)(?!["\w])/g;
+ regClass = /\.([\w-]+)/i;
+ regReference = /(@[\w$_][\w$_\d]+)/i;
+ regAttrDfn = /(\[([\w-!]+(="?([^"]|\\")+"?)? {0,})+\])/ig;
+ regAttrs = /([\w-!]+(="([^"]|\\")+")?)/g;
+ regAttr = /([\w-!]+)(="?((([\w]+(\[.*?\])+)|[^"\]]|\\")+)"?)?/i;
+ regCBrace = /\{(([^\}]|\\\})+)\}/i;
+ regExclamation = /(?:([^\\]|^))!([^!]|\\!)+!/g;
+ regEvents = /\~[\w$]+(=[\w$]+)?/g;
+ regEvent = /\~([\w$]+)=([\w$]+)/i;
+ regDatas = /&[\w$]+(=[\w$]+)?/g;
+ regData = /&([\w$]+)(=([\w$]+))?/i;
+ return createHTMLBlock;
+ })();
+
+}).call(this);